diff options
author | ZyX <kp-pav@yandex.ru> | 2015-04-25 18:47:31 +0300 |
---|---|---|
committer | ZyX <kp-pav@yandex.ru> | 2015-10-08 21:59:51 +0300 |
commit | 244dbe3a77bf548f73d8781da7327f30e818b08a (patch) | |
tree | 8777a25447be219fe351106cfef37670e0278ddd | |
parent | 0fdaab995ed95250b13058a717d5bc562e1834c8 (diff) | |
download | rneovim-244dbe3a77bf548f73d8781da7327f30e818b08a.tar.gz rneovim-244dbe3a77bf548f73d8781da7327f30e818b08a.tar.bz2 rneovim-244dbe3a77bf548f73d8781da7327f30e818b08a.zip |
viminfo: First version of ShaDa file dumping
What works:
1. ShaDa file dumping: header, registers, jump list, history, search patterns,
substitute strings, variables.
2. ShaDa file reading: registers, global marks, variables.
Most was not tested.
TODO:
1. Merging.
2. Reading history, local marks, jump and buffer lists.
3. Documentation update.
4. Converting some data from &encoding.
5. Safer variant of dumping viminfo (dump to temporary file then rename).
6. Removing old viminfo code (currently masked with `#if 0` in a ShaDa file for
reference).
38 files changed, 4485 insertions, 1822 deletions
diff --git a/contrib/YouCompleteMe/ycm_extra_conf.py b/contrib/YouCompleteMe/ycm_extra_conf.py index 7c54677c8f..12ad080143 100644 --- a/contrib/YouCompleteMe/ycm_extra_conf.py +++ b/contrib/YouCompleteMe/ycm_extra_conf.py @@ -9,7 +9,7 @@ def DirectoryOfThisScript(): def GetDatabase(): compilation_database_folder = os.path.join(DirectoryOfThisScript(), - '..', 'build') + '..', '..', 'build') if os.path.exists(compilation_database_folder): return ycm_core.CompilationDatabase(compilation_database_folder) return None diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 897137ff42..085f6f21a8 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -6770,10 +6770,14 @@ A jump table for the options with a short description can be found at |Q_op|. e.g., for Unix: "r/tmp". Case is ignored. Maximum length of each 'r' argument is 50 characters. *viminfo-s* - s Maximum size of an item in Kbyte. If zero then registers are - not saved. Currently only applies to registers. The default - "s10" will exclude registers with more than 10 Kbyte of text. - Also see the '<' item above: line count limit. + s Maximum size of an item contents in KiB. If zero then nothing + is saved. Unlike Vim this applies to all items, except for + the buffer list and header. Full item size is off by three + unsigned integers: with `s10` maximum item size may be 1 byte + (type: 7-bit integer) + 9 bytes (timestamp: up to 64-bit + integer) + 3 bytes (item size: up to 16-bit integer because + 2^8 < 10240 < 2^16) + 10240 bytes (requested maximum item + contents size) = 10253 bytes. Example: > :set viminfo='50,<1000,s100,:0,n~/vim/viminfo @@ -6782,7 +6786,8 @@ A jump table for the options with a short description can be found at |Q_op|. edited. <1000 Contents of registers (up to 1000 lines each) will be remembered. - s100 Registers with more than 100 Kbyte text are skipped. + s100 Items with contents occupying more then 100 KiB are + skipped. :0 Command-line history will not be saved. n~/vim/viminfo The name of the file to use is "~/vim/viminfo". no / Since '/' is not specified, the default will be used, diff --git a/scripts/shadacat.py b/scripts/shadacat.py new file mode 100755 index 0000000000..8f5ed276f8 --- /dev/null +++ b/scripts/shadacat.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3.4 + +import sys +import codecs + +from enum import Enum +from datetime import datetime +from functools import reduce + +import msgpack + + +class EntryTypes(Enum): + Unknown = -1 + Missing = 0 + Header = 1 + SearchPattern = 2 + SubString = 3 + HistoryEntry = 4 + Register = 5 + Variable = 6 + GlobalMark = 7 + Jump = 8 + BufferList = 9 + LocalMark = 10 + + +def strtrans_errors(e): + if not isinstance(e, UnicodeDecodeError): + raise NotImplementedError('don’t know how to handle {0} error'.format( + e.__class__.__name__)) + return '<{0:x}>'.format(reduce((lambda a, b: a*0x100+b), + list(e.object[e.start:e.end]))), e.end + + +codecs.register_error('strtrans', strtrans_errors) + + +def idfunc(o): + return o + + +class CharInt(int): + def __repr__(self): + return super(CharInt, self).__repr__() + ' (\'%s\')' % chr(self) + + +ctable = { + bytes: lambda s: s.decode('utf-8', 'strtrans'), + dict: lambda d: dict((mnormalize(k), mnormalize(v)) for k, v in d.items()), + list: lambda l: list(mnormalize(i) for i in l), + int: lambda n: CharInt(n) if 0x20 <= n <= 0x7E else n, +} + + +def mnormalize(o): + return ctable.get(type(o), idfunc)(o) + + +with open(sys.argv[1], 'rb') as fp: + unpacker = msgpack.Unpacker(file_like=fp) + while True: + try: + typ = EntryTypes(unpacker.unpack()) + except msgpack.OutOfData: + break + else: + timestamp = unpacker.unpack() + time = datetime.fromtimestamp(timestamp) + length = unpacker.unpack() + entry = unpacker.unpack() + print('{0:13} {1} {2:5} {3!r}'.format( + typ.name, time.isoformat(), length, mnormalize(entry))) diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index f29deb53f9..a0f14ac7a4 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -69,6 +69,8 @@ #define ADD(array, item) \ kv_push(Object, array, item) +#define STATIC_CSTR_AS_STRING(s) ((String) {.data = s, .size = sizeof(s) - 1}) + // Helpers used by the generated msgpack-rpc api wrappers #define api_init_boolean #define api_init_integer diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua index af801c6f1a..3d8a75febd 100644 --- a/src/nvim/auevents.lua +++ b/src/nvim/auevents.lua @@ -84,7 +84,7 @@ return { 'User', -- user defined autocommand 'VimEnter', -- after starting Vim 'VimLeave', -- before exiting Vim - 'VimLeavePre', -- before exiting Vim and writing .viminfo + 'VimLeavePre', -- before exiting Vim and writing ShaDa file 'VimResized', -- after Vim window was resized 'WinEnter', -- after entering a window 'WinLeave', -- before leaving a window diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index b212e75283..5ebeec1b70 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -73,6 +73,7 @@ #include "nvim/undo.h" #include "nvim/version.h" #include "nvim/window.h" +#include "nvim/shada.h" #include "nvim/os/os.h" #include "nvim/os/time.h" #include "nvim/os/input.h" @@ -555,6 +556,12 @@ static void free_buffer(buf_T *buf) free_buffer_stuff(buf, TRUE); unref_var_dict(buf->b_vars); aubuflocal_remove(buf); + free_fmark(buf->b_last_cursor); + free_fmark(buf->b_last_insert); + free_fmark(buf->b_last_change); + for (size_t i = 0; i < NMARKS; i++) { + free_fmark(buf->b_namedm[i]); + } if (autocmd_busy) { // Do not free the buffer structure while autocommands are executing, // it's still needed. Free it when autocmd_busy is reset. @@ -4164,93 +4171,6 @@ chk_modeline ( return retval; } -int read_viminfo_bufferlist(vir_T *virp, int writing) -{ - char_u *tab; - linenr_T lnum; - colnr_T col; - buf_T *buf; - char_u *sfname; - char_u *xline; - - /* Handle long line and escaped characters. */ - xline = viminfo_readstring(virp, 1, FALSE); - - /* don't read in if there are files on the command-line or if writing: */ - if (xline != NULL && !writing && ARGCOUNT == 0 - && find_viminfo_parameter('%') != NULL) { - /* Format is: <fname> Tab <lnum> Tab <col>. - * Watch out for a Tab in the file name, work from the end. */ - lnum = 0; - col = 0; - tab = vim_strrchr(xline, '\t'); - if (tab != NULL) { - *tab++ = '\0'; - col = (colnr_T)atoi((char *)tab); - tab = vim_strrchr(xline, '\t'); - if (tab != NULL) { - *tab++ = '\0'; - lnum = atol((char *)tab); - } - } - - /* Expand "~/" in the file name at "line + 1" to a full path. - * Then try shortening it by comparing with the current directory */ - expand_env(xline, NameBuff, MAXPATHL); - sfname = path_shorten_fname_if_possible(NameBuff); - - buf = buflist_new(NameBuff, sfname, (linenr_T)0, BLN_LISTED); - if (buf != NULL) { /* just in case... */ - buf->b_last_cursor.lnum = lnum; - buf->b_last_cursor.col = col; - buflist_setfpos(buf, curwin, lnum, col, FALSE); - } - } - xfree(xline); - - return viminfo_readline(virp); -} - -void write_viminfo_bufferlist(FILE *fp) -{ - char_u *line; - int max_buffers; - - if (find_viminfo_parameter('%') == NULL) - return; - - /* Without a number -1 is returned: do all buffers. */ - max_buffers = get_viminfo_parameter('%'); - - /* Allocate room for the file name, lnum and col. */ -#define LINE_BUF_LEN (MAXPATHL + 40) - line = xmalloc(LINE_BUF_LEN); - - FOR_ALL_TAB_WINDOWS(tp, win) { - set_last_cursor(win); - } - - fputs(_("\n# Buffer list:\n"), fp); - FOR_ALL_BUFFERS(buf) { - if (buf->b_fname == NULL - || !buf->b_p_bl - || bt_quickfix(buf) - || removable(buf->b_ffname)) - continue; - - if (max_buffers-- == 0) - break; - putc('%', fp); - home_replace(NULL, buf->b_ffname, line, MAXPATHL, TRUE); - vim_snprintf_add((char *)line, LINE_BUF_LEN, "\t%" PRId64 "\t%d", - (int64_t)buf->b_last_cursor.lnum, - buf->b_last_cursor.col); - viminfo_writestring(fp, line); - } - xfree(line); -} - - /* * Return special buffer name. * Returns NULL when the buffer has a normal file name. diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 98fbef9c87..6fbdea2c62 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -327,15 +327,6 @@ typedef struct { bool vc_fail; /* fail for invalid char, don't use '?' */ } vimconv_T; -/* - * Structure used for reading from the viminfo file. - */ -typedef struct { - char_u *vir_line; /* text of the current line */ - FILE *vir_fd; /* file descriptor */ - vimconv_T vir_conv; /* encoding conversion */ -} vir_T; - #define CONV_NONE 0 #define CONV_TO_UTF8 1 #define CONV_9_TO_UTF8 2 @@ -515,16 +506,16 @@ struct file_buffer { uint64_t b_orig_size; /* size of original file in bytes */ int b_orig_mode; /* mode of original file */ - pos_T b_namedm[NMARKS]; /* current named marks (mark.c) */ + fmark_T b_namedm[NMARKS]; /* current named marks (mark.c) */ /* These variables are set when VIsual_active becomes FALSE */ visualinfo_T b_visual; int b_visual_mode_eval; /* b_visual.vi_mode for visualmode() */ - pos_T b_last_cursor; /* cursor position when last unloading this + fmark_T b_last_cursor; /* cursor position when last unloading this buffer */ - pos_T b_last_insert; /* where Insert mode was left */ - pos_T b_last_change; /* position of last change: '. mark */ + fmark_T b_last_insert; /* where Insert mode was left */ + fmark_T b_last_change; /* position of last change: '. mark */ /* * the changelist contains old change positions @@ -553,7 +544,7 @@ struct file_buffer { pos_T b_op_start_orig; // used for Insstart_orig pos_T b_op_end; - bool b_marks_read; /* Have we read viminfo marks yet? */ + bool b_marks_read; /* Have we read ShaDa marks yet? */ /* * The following only used in undo.c. diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 427623e052..310191ba06 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -60,6 +60,7 @@ #include "nvim/undo.h" #include "nvim/window.h" #include "nvim/event/loop.h" +#include "nvim/mark.h" #include "nvim/os/input.h" #include "nvim/os/time.h" @@ -6991,8 +6992,9 @@ ins_esc ( curwin->w_set_curswant = TRUE; /* Remember the last Insert position in the '^ mark. */ - if (!cmdmod.keepjumps) - curbuf->b_last_insert = curwin->w_cursor; + if (!cmdmod.keepjumps) { + RESET_FMARK(&curbuf->b_last_insert, curwin->w_cursor, curbuf->b_fnum); + } /* * The cursor should end up on the last inserted character. diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 65afd19bbe..5a7d4702d2 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -354,7 +354,7 @@ typedef struct { typedef enum { VAR_FLAVOUR_DEFAULT, /* doesn't start with uppercase */ VAR_FLAVOUR_SESSION, /* starts with uppercase, some lower */ - VAR_FLAVOUR_VIMINFO /* all uppercase */ + VAR_FLAVOUR_SHADA /* all uppercase */ } var_flavour_T; /* values for vv_flags: */ @@ -10482,6 +10482,7 @@ static void f_has(typval_T *argvars, typval_T *rettv) "scrollbind", "showcmd", "cmdline_info", + "shada", "signs", "smartindent", "startuptime", @@ -10498,7 +10499,6 @@ static void f_has(typval_T *argvars, typval_T *rettv) "title", "user-commands", /* was accidentally included in 5.4 */ "user_commands", - "viminfo", "vertsplit", "virtualedit", "visual", @@ -20712,107 +20712,62 @@ static var_flavour_T var_flavour(char_u *varname) while (*(++p)) if (ASCII_ISLOWER(*p)) return VAR_FLAVOUR_SESSION; - return VAR_FLAVOUR_VIMINFO; + return VAR_FLAVOUR_SHADA; } else return VAR_FLAVOUR_DEFAULT; } -/* - * Restore global vars that start with a capital from the viminfo file - */ -int read_viminfo_varlist(vir_T *virp, int writing) -{ - char_u *tab; - int type = VAR_NUMBER; - typval_T tv; - - if (!writing && (find_viminfo_parameter('!') != NULL)) { - tab = vim_strchr(virp->vir_line + 1, '\t'); - if (tab != NULL) { - *tab++ = NUL; /* isolate the variable name */ - switch (*tab) { - case 'S': type = VAR_STRING; break; - case 'F': type = VAR_FLOAT; break; - case 'D': type = VAR_DICT; break; - case 'L': type = VAR_LIST; break; - } - - tab = vim_strchr(tab, '\t'); - if (tab != NULL) { - tv.v_type = type; - if (type == VAR_STRING || type == VAR_DICT || type == VAR_LIST) - tv.vval.v_string = viminfo_readstring(virp, - (int)(tab - virp->vir_line + 1), TRUE); - else if (type == VAR_FLOAT) - (void)string2float(tab + 1, &tv.vval.v_float); - else - tv.vval.v_number = atol((char *)tab + 1); - if (type == VAR_DICT || type == VAR_LIST) { - typval_T *etv = eval_expr(tv.vval.v_string, NULL); - - if (etv == NULL) - /* Failed to parse back the dict or list, use it as a - * string. */ - tv.v_type = VAR_STRING; - else { - xfree(tv.vval.v_string); - tv = *etv; - xfree(etv); - } - } - - set_var(virp->vir_line + 1, &tv, FALSE); - - if (tv.v_type == VAR_STRING) - xfree(tv.vval.v_string); - else if (tv.v_type == VAR_DICT || tv.v_type == VAR_LIST) - clear_tv(&tv); - } +/// Iterate over global variables +/// +/// @warning No modifications to global variable dictionary must be performed +/// while iteration is in progress. +/// +/// @param[in] iter Iterator. Pass NULL to start iteration. +/// @param[out] name Variable name. +/// @param[out] rettv Variable value. +/// +/// @return Pointer that needs to be passed to next `var_shada_iter` invocation +/// or NULL to indicate that iteration is over. +const void *var_shada_iter(const void *const iter, const char **const name, + typval_T *rettv) + FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(2, 3) +{ + const hashitem_T *hi; + const hashitem_T *hifirst = globvarht.ht_array; + const size_t hinum = (size_t) globvarht.ht_mask + 1; + *name = NULL; + if (iter == NULL) { + hi = globvarht.ht_array; + while ((HASHITEM_EMPTY(hi) + || var_flavour(HI2DI(hi)->di_key) != VAR_FLAVOUR_SHADA) + && (size_t) (hi - hifirst) < hinum) { + hi++; + } + if (HASHITEM_EMPTY(hi) + || var_flavour(HI2DI(hi)->di_key) != VAR_FLAVOUR_SHADA) { + *rettv = (typval_T) {.v_type = VAR_UNKNOWN}; + return NULL; } + } else { + hi = (const hashitem_T *) iter; } - - return viminfo_readline(virp); + *name = (char *) HI2DI(hi)->di_key; + copy_tv(&(HI2DI(hi)->di_tv), rettv); + while ((size_t) (++hi - hifirst) < hinum) { + if (!HASHITEM_EMPTY(hi) + && var_flavour(HI2DI(hi)->di_key) == VAR_FLAVOUR_SHADA) { + return hi; + } + } + return NULL; } -/* - * Write global vars that start with a capital to the viminfo file - */ -void write_viminfo_varlist(FILE *fp) +void var_set_global(const char *const name, typval_T vartv) { - hashitem_T *hi; - dictitem_T *this_var; - int todo; - char *s; - char_u *p; - - if (find_viminfo_parameter('!') == NULL) - return; - - fputs(_("\n# global variables:\n"), fp); - - todo = (int)globvarht.ht_used; - for (hi = globvarht.ht_array; todo > 0; ++hi) { - if (!HASHITEM_EMPTY(hi)) { - --todo; - this_var = HI2DI(hi); - if (var_flavour(this_var->di_key) == VAR_FLAVOUR_VIMINFO) { - switch (this_var->di_tv.v_type) { - case VAR_STRING: s = "STR"; break; - case VAR_NUMBER: s = "NUM"; break; - case VAR_FLOAT: s = "FLO"; break; - case VAR_DICT: s = "DIC"; break; - case VAR_LIST: s = "LIS"; break; - default: continue; - } - fprintf(fp, "!%s\t%s\t", this_var->di_key, s); - p = (char_u *) echo_string(&this_var->di_tv, NULL); - if (p != NULL) { - viminfo_writestring(fp, p); - } - xfree(p); - } - } - } + funccall_T *const saved_current_funccal = current_funccal; + current_funccal = NULL; + set_var((char_u *) name, &vartv, false); + current_funccal = saved_current_funccal; } int store_session_globals(FILE *fd) diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 81abf2fa63..262150a8c0 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -67,6 +67,9 @@ #include "nvim/os/os.h" #include "nvim/os/shell.h" #include "nvim/os/input.h" +#include "nvim/os/time.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" /* * Struct to hold the sign properties. @@ -1391,550 +1394,6 @@ void append_redir(char_u *buf, int buflen, char_u *opt, char_u *fname) (char *)opt, (char *)fname); } - -static int viminfo_errcnt; - -static int no_viminfo(void) -{ - /* "vim -i NONE" does not read or write a viminfo file */ - return use_viminfo != NULL && STRCMP(use_viminfo, "NONE") == 0; -} - -/* - * Report an error for reading a viminfo file. - * Count the number of errors. When there are more than 10, return TRUE. - */ -int viminfo_error(char *errnum, char *message, char_u *line) -{ - vim_snprintf((char *)IObuff, IOSIZE, _("%sviminfo: %s in line: "), - errnum, message); - STRNCAT(IObuff, line, IOSIZE - STRLEN(IObuff) - 1); - if (IObuff[STRLEN(IObuff) - 1] == '\n') - IObuff[STRLEN(IObuff) - 1] = NUL; - emsg(IObuff); - if (++viminfo_errcnt >= 10) { - EMSG(_("E136: viminfo: Too many errors, skipping rest of file")); - return TRUE; - } - return FALSE; -} - -/* - * read_viminfo() -- Read the viminfo file. Registers etc. which are already - * set are not over-written unless "flags" includes VIF_FORCEIT. -- webb - */ -int -read_viminfo ( - char_u *file, /* file name or NULL to use default name */ - int flags /* VIF_WANT_INFO et al. */ -) -{ - FILE *fp; - char_u *fname; - - if (no_viminfo()) - return FAIL; - - fname = viminfo_filename(file); /* get file name in allocated buffer */ - fp = mch_fopen((char *)fname, READBIN); - - if (p_verbose > 0) { - verbose_enter(); - smsg(_("Reading viminfo file \"%s\"%s%s%s"), - fname, - (flags & VIF_WANT_INFO) ? _(" info") : "", - (flags & VIF_WANT_MARKS) ? _(" marks") : "", - (flags & VIF_GET_OLDFILES) ? _(" oldfiles") : "", - fp == NULL ? _(" FAILED") : ""); - verbose_leave(); - } - - xfree(fname); - if (fp == NULL) - return FAIL; - - viminfo_errcnt = 0; - do_viminfo(fp, NULL, flags); - - fclose(fp); - return OK; -} - -/* - * Write the viminfo file. The old one is read in first so that effectively a - * merge of current info and old info is done. This allows multiple vims to - * run simultaneously, without losing any marks etc. - * If "forceit" is TRUE, then the old file is not read in, and only internal - * info is written to the file. - */ -void write_viminfo(char_u *file, int forceit) -{ - char_u *fname; - FILE *fp_in = NULL; /* input viminfo file, if any */ - FILE *fp_out = NULL; /* output viminfo file */ - char_u *tempname = NULL; /* name of temp viminfo file */ - char_u *wp; -#if defined(UNIX) - mode_t umask_save; -#endif - - if (no_viminfo()) - return; - - fname = viminfo_filename(file); /* may set to default if NULL */ - - fp_in = mch_fopen((char *)fname, READBIN); - if (fp_in == NULL) { - /* if it does exist, but we can't read it, don't try writing */ - if (os_file_exists(fname)) - goto end; -#if defined(UNIX) - /* - * For Unix we create the .viminfo non-accessible for others, - * because it may contain text from non-accessible documents. - */ - umask_save = umask(077); -#endif - fp_out = mch_fopen((char *)fname, WRITEBIN); -#if defined(UNIX) - (void)umask(umask_save); -#endif - } else { - /* - * There is an existing viminfo file. Create a temporary file to - * write the new viminfo into, in the same directory as the - * existing viminfo file, which will be renamed later. - */ -#ifdef UNIX - /* - * For Unix we check the owner of the file. It's not very nice to - * overwrite a user's viminfo file after a "su root", with a - * viminfo file that the user can't read. - */ - - FileInfo old_info; // FileInfo of existing viminfo file - if (os_fileinfo((char *)fname, &old_info) - && getuid() != ROOT_UID - && !(old_info.stat.st_uid == getuid() - ? (old_info.stat.st_mode & 0200) - : (old_info.stat.st_gid == getgid() - ? (old_info.stat.st_mode & 0020) - : (old_info.stat.st_mode & 0002)))) { - int tt = msg_didany; - - /* avoid a wait_return for this message, it's annoying */ - EMSG2(_("E137: Viminfo file is not writable: %s"), fname); - msg_didany = tt; - fclose(fp_in); - goto end; - } -#endif - - // Make tempname - tempname = (char_u *)modname((char *)fname, ".tmp", FALSE); - if (tempname != NULL) { - /* - * Check if tempfile already exists. Never overwrite an - * existing file! - */ - if (os_file_exists(tempname)) { - /* - * Try another name. Change one character, just before - * the extension. - */ - wp = tempname + STRLEN(tempname) - 5; - if (wp < path_tail(tempname)) /* empty file name? */ - wp = path_tail(tempname); - for (*wp = 'z'; os_file_exists(tempname); --*wp) { - /* - * They all exist? Must be something wrong! Don't - * write the viminfo file then. - */ - if (*wp == 'a') { - xfree(tempname); - tempname = NULL; - break; - } - } - } - } - - if (tempname != NULL) { - int fd; - - /* Use os_open() to be able to use O_NOFOLLOW and set file - * protection: - * Unix: same as original file, but strip s-bit. Reset umask to - * avoid it getting in the way. - * Others: r&w for user only. */ -# ifdef UNIX - umask_save = umask(0); - fd = os_open((char *)tempname, - O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW, - (int)((old_info.stat.st_mode & 0777) | 0600)); - (void)umask(umask_save); -# else - fd = os_open((char *)tempname, - O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW, 0600); -# endif - if (fd < 0) - fp_out = NULL; - else - fp_out = fdopen(fd, WRITEBIN); - - /* - * If we can't create in the same directory, try creating a - * "normal" temp file. - */ - if (fp_out == NULL) { - xfree(tempname); - if ((tempname = vim_tempname()) != NULL) - fp_out = mch_fopen((char *)tempname, WRITEBIN); - } - -#ifdef UNIX - /* - * Make sure the owner can read/write it. This only works for - * root. - */ - if (fp_out != NULL) { - os_fchown(fileno(fp_out), old_info.stat.st_uid, old_info.stat.st_gid); - } -#endif - } - } - - /* - * Check if the new viminfo file can be written to. - */ - if (fp_out == NULL) { - EMSG2(_("E138: Can't write viminfo file %s!"), - (fp_in == NULL || tempname == NULL) ? fname : tempname); - if (fp_in != NULL) - fclose(fp_in); - goto end; - } - - if (p_verbose > 0) { - verbose_enter(); - smsg(_("Writing viminfo file \"%s\""), fname); - verbose_leave(); - } - - viminfo_errcnt = 0; - do_viminfo(fp_in, fp_out, forceit ? 0 : (VIF_WANT_INFO | VIF_WANT_MARKS)); - - fclose(fp_out); /* errors are ignored !? */ - if (fp_in != NULL) { - fclose(fp_in); - - /* In case of an error keep the original viminfo file. Otherwise - * rename the newly written file. Give an error if that fails. */ - if (viminfo_errcnt == 0 && vim_rename(tempname, fname) == -1) { - viminfo_errcnt++; - EMSG2(_("E886: Can't rename viminfo file to %s!"), fname); - } - if (viminfo_errcnt > 0) { - os_remove((char *)tempname); - } - } - -end: - xfree(fname); - xfree(tempname); -} - -/* - * Get the viminfo file name to use. - * If "file" is given and not empty, use it (has already been expanded by - * cmdline functions). - * Otherwise use "-i file_name", value from 'viminfo' or the default, and - * expand environment variables. - * Returns an allocated string. - */ -static char_u *viminfo_filename(char_u *file) -{ - if (file == NULL || *file == NUL) { - if (use_viminfo != NULL) - file = use_viminfo; - else if ((file = find_viminfo_parameter('n')) == NULL || *file == NUL) { -#ifdef VIMINFO_FILE2 - // don't use $HOME when not defined (turned into "c:/"!). - if (!os_env_exists("HOME")) { - // don't use $VIM when not available. - expand_env((char_u *)"$VIM", NameBuff, MAXPATHL); - if (STRCMP("$VIM", NameBuff) != 0) /* $VIM was expanded */ - file = (char_u *)VIMINFO_FILE2; - else - file = (char_u *)VIMINFO_FILE; - } else -#endif - file = (char_u *)VIMINFO_FILE; - } - expand_env(file, NameBuff, MAXPATHL); - file = NameBuff; - } - return vim_strsave(file); -} - -/* - * do_viminfo() -- Should only be called from read_viminfo() & write_viminfo(). - */ -static void do_viminfo(FILE *fp_in, FILE *fp_out, int flags) -{ - int count = 0; - int eof = FALSE; - vir_T vir; - int merge = FALSE; - - vir.vir_line = xmalloc(LSIZE); - vir.vir_fd = fp_in; - vir.vir_conv.vc_type = CONV_NONE; - - if (fp_in != NULL) { - if (flags & VIF_WANT_INFO) { - eof = read_viminfo_up_to_marks(&vir, - flags & VIF_FORCEIT, fp_out != NULL); - merge = TRUE; - } else if (flags != 0) - /* Skip info, find start of marks */ - while (!(eof = viminfo_readline(&vir)) - && vir.vir_line[0] != '>') - ; - } - if (fp_out != NULL) { - /* Write the info: */ - fprintf(fp_out, _("# This viminfo file was generated by Nvim %s.\n"), - mediumVersion); - fputs(_("# You may edit it if you're careful!\n\n"), fp_out); - fputs(_("# Value of 'encoding' when this file was written\n"), fp_out); - fprintf(fp_out, "*encoding=%s\n\n", p_enc); - write_viminfo_search_pattern(fp_out); - write_viminfo_sub_string(fp_out); - write_viminfo_history(fp_out, merge); - write_viminfo_registers(fp_out); - write_viminfo_varlist(fp_out); - write_viminfo_filemarks(fp_out); - write_viminfo_bufferlist(fp_out); - count = write_viminfo_marks(fp_out); - } - if (fp_in != NULL - && (flags & (VIF_WANT_MARKS | VIF_GET_OLDFILES | VIF_FORCEIT))) - copy_viminfo_marks(&vir, fp_out, count, eof, flags); - - xfree(vir.vir_line); - if (vir.vir_conv.vc_type != CONV_NONE) - convert_setup(&vir.vir_conv, NULL, NULL); -} - -/* - * read_viminfo_up_to_marks() -- Only called from do_viminfo(). Reads in the - * first part of the viminfo file which contains everything but the marks that - * are local to a file. Returns TRUE when end-of-file is reached. -- webb - */ -static int read_viminfo_up_to_marks(vir_T *virp, int forceit, int writing) -{ - int eof; - - prepare_viminfo_history(forceit ? 9999 : 0, writing); - eof = viminfo_readline(virp); - while (!eof && virp->vir_line[0] != '>') { - switch (virp->vir_line[0]) { - /* Characters reserved for future expansion, ignored now */ - case '+': /* "+40 /path/dir file", for running vim without args */ - case '|': /* to be defined */ - case '^': /* to be defined */ - case '<': /* long line - ignored */ - /* A comment or empty line. */ - case NUL: - case '\r': - case '\n': - case '#': - eof = viminfo_readline(virp); - break; - case '*': /* "*encoding=value" */ - eof = viminfo_encoding(virp); - break; - case '!': /* global variable */ - eof = read_viminfo_varlist(virp, writing); - break; - case '%': /* entry for buffer list */ - eof = read_viminfo_bufferlist(virp, writing); - break; - case '"': - eof = read_viminfo_register(virp, forceit); - break; - case '/': /* Search string */ - case '&': /* Substitute search string */ - case '~': /* Last search string, followed by '/' or '&' */ - eof = read_viminfo_search_pattern(virp, forceit); - break; - case '$': - eof = read_viminfo_sub_string(virp, forceit); - break; - case ':': - case '?': - case '=': - case '@': - eof = read_viminfo_history(virp, writing); - break; - case '-': - case '\'': - eof = read_viminfo_filemark(virp, forceit); - break; - default: - if (viminfo_error("E575: ", _("Illegal starting char"), - virp->vir_line)) - eof = TRUE; - else - eof = viminfo_readline(virp); - break; - } - } - - /* Finish reading history items. */ - if (!writing) - finish_viminfo_history(); - - /* Change file names to buffer numbers for fmarks. */ - FOR_ALL_BUFFERS(buf) { - fmarks_check_names(buf); - } - - return eof; -} - -/* - * Compare the 'encoding' value in the viminfo file with the current value of - * 'encoding'. If different and the 'c' flag is in 'viminfo', setup for - * conversion of text with iconv() in viminfo_readstring(). - */ -static int viminfo_encoding(vir_T *virp) -{ - char_u *p; - int i; - - if (get_viminfo_parameter('c') != 0) { - p = vim_strchr(virp->vir_line, '='); - if (p != NULL) { - /* remove trailing newline */ - ++p; - for (i = 0; vim_isprintc(p[i]); ++i) - ; - p[i] = NUL; - - convert_setup(&virp->vir_conv, p, p_enc); - } - } - return viminfo_readline(virp); -} - -/* - * Read a line from the viminfo file. - * Returns TRUE for end-of-file; - */ -int viminfo_readline(vir_T *virp) -{ - return vim_fgets(virp->vir_line, LSIZE, virp->vir_fd); -} - -/* - * check string read from viminfo file - * remove '\n' at the end of the line - * - replace CTRL-V CTRL-V with CTRL-V - * - replace CTRL-V 'n' with '\n' - * - * Check for a long line as written by viminfo_writestring(). - * - * Return the string in allocated memory. - */ -char_u * -viminfo_readstring ( - vir_T *virp, - int off, /* offset for virp->vir_line */ - int convert /* convert the string */ -) - FUNC_ATTR_NONNULL_RET -{ - char_u *retval; - char_u *s, *d; - - if (virp->vir_line[off] == Ctrl_V && ascii_isdigit(virp->vir_line[off + 1])) { - ssize_t len = atol((char *)virp->vir_line + off + 1); - retval = xmalloc(len); - // TODO(philix): change type of vim_fgets() size argument to size_t - (void)vim_fgets(retval, (int)len, virp->vir_fd); - s = retval + 1; /* Skip the leading '<' */ - } else { - retval = vim_strsave(virp->vir_line + off); - s = retval; - } - - /* Change CTRL-V CTRL-V to CTRL-V and CTRL-V n to \n in-place. */ - d = retval; - while (*s != NUL && *s != '\n') { - if (s[0] == Ctrl_V && s[1] != NUL) { - if (s[1] == 'n') - *d++ = '\n'; - else - *d++ = Ctrl_V; - s += 2; - } else - *d++ = *s++; - } - *d = NUL; - - if (convert && virp->vir_conv.vc_type != CONV_NONE && *retval != NUL) { - d = string_convert(&virp->vir_conv, retval, NULL); - if (d != NULL) { - xfree(retval); - retval = d; - } - } - - return retval; -} - -/* - * write string to viminfo file - * - replace CTRL-V with CTRL-V CTRL-V - * - replace '\n' with CTRL-V 'n' - * - add a '\n' at the end - * - * For a long line: - * - write " CTRL-V <length> \n " in first line - * - write " < <string> \n " in second line - */ -void viminfo_writestring(FILE *fd, char_u *p) -{ - int c; - char_u *s; - int len = 0; - - for (s = p; *s != NUL; ++s) { - if (*s == Ctrl_V || *s == '\n') - ++len; - ++len; - } - - /* If the string will be too long, write its length and put it in the next - * line. Take into account that some room is needed for what comes before - * the string (e.g., variable name). Add something to the length for the - * '<', NL and trailing NUL. */ - if (len > LSIZE / 2) - fprintf(fd, "\026%d\n<", len + 3); - - while ((c = *p++) != NUL) { - if (c == Ctrl_V || c == '\n') { - putc(Ctrl_V, fd); - if (c == '\n') - c = 'n'; - } - putc(c, fd); - } - putc('\n', fd); -} - void print_line_no_prefix(linenr_T lnum, int use_number, int list) { char_u numbuf[30]; @@ -3364,8 +2823,36 @@ int check_secure(void) return FALSE; } -static char_u *old_sub = NULL; /* previous substitute pattern */ -static int global_need_beginline; /* call beginline() after ":g" */ +/// Previous substitute replacement string +static SubReplacementString old_sub = {NULL, 0, NULL}; + +static int global_need_beginline; // call beginline() after ":g" + +/// Get old substitute replacement string +/// +/// @param[out] ret_sub Location where old string will be saved. +void sub_get_replacement(SubReplacementString *const ret_sub) + FUNC_ATTR_NONNULL_ALL +{ + *ret_sub = old_sub; +} + +/// Set substitute string and timestamp +/// +/// @warning `sub` must be in allocated memory. It is not copied. +/// +/// @param[in] sub New replacement string. +void sub_set_replacement(SubReplacementString sub) +{ + xfree(old_sub.sub); + if (sub.additional_elements != old_sub.additional_elements) { + if (old_sub.additional_elements != NULL) { + api_free_array(*old_sub.additional_elements); + xfree(old_sub.additional_elements); + } + } + old_sub = sub; +} /* do_sub() * @@ -3473,16 +2960,19 @@ void do_sub(exarg_T *eap) } if (!eap->skip) { - xfree(old_sub); - old_sub = vim_strsave(sub); + sub_set_replacement((SubReplacementString) { + .sub = xstrdup((char *) sub), + .timestamp = os_time(), + .additional_elements = NULL, + }); } } else if (!eap->skip) { /* use previous pattern and substitution */ - if (old_sub == NULL) { /* there is no previous command */ + if (old_sub.sub == NULL) { /* there is no previous command */ EMSG(_(e_nopresub)); return; } pat = NULL; /* search_regcomp() will use previous pattern */ - sub = old_sub; + sub = (char_u *) old_sub.sub; /* Vi compatibility quirk: repeating with ":s" keeps the cursor in the * last column after using "$". */ @@ -4501,27 +3991,10 @@ void global_exe(char_u *cmd) msgmore(curbuf->b_ml.ml_line_count - old_lcount); } -int read_viminfo_sub_string(vir_T *virp, int force) -{ - if (force) - xfree(old_sub); - if (force || old_sub == NULL) - old_sub = viminfo_readstring(virp, 1, TRUE); - return viminfo_readline(virp); -} - -void write_viminfo_sub_string(FILE *fp) -{ - if (get_viminfo_parameter('/') != 0 && old_sub != NULL) { - fputs(_("\n# Last Substitute String:\n$"), fp); - viminfo_writestring(fp, old_sub); - } -} - #if defined(EXITFREE) void free_old_sub(void) { - xfree(old_sub); + sub_set_replacement((SubReplacementString) {NULL, 0, NULL}); } #endif diff --git a/src/nvim/ex_cmds.h b/src/nvim/ex_cmds.h index eabbbd15ac..71fc100e41 100644 --- a/src/nvim/ex_cmds.h +++ b/src/nvim/ex_cmds.h @@ -3,6 +3,9 @@ #include <stdbool.h> +#include "nvim/os/time.h" +#include "nvim/api/private/defs.h" + /* flags for do_ecmd() */ #define ECMD_HIDE 0x01 /* don't free the current buffer */ #define ECMD_SET_HELP 0x02 /* set b_help flag of (new) buffer before @@ -16,11 +19,12 @@ #define ECMD_LAST (linenr_T)-1 /* use last position in all files */ #define ECMD_ONE (linenr_T)1 /* use first line */ -/* flags for read_viminfo() and children */ -#define VIF_WANT_INFO 1 /* load non-mark info */ -#define VIF_WANT_MARKS 2 /* load file marks */ -#define VIF_FORCEIT 4 /* overwrite info already read */ -#define VIF_GET_OLDFILES 8 /* load v:oldfiles */ +/// Previous :substitute replacement string definition +typedef struct { + char *sub; ///< Previous replacement string. + Timestamp timestamp; ///< Time when it was last set. + Array *additional_elements; ///< Additional data left from ShaDa file. +} SubReplacementString; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ex_cmds.h.generated.h" diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index f7162896ff..c45e641706 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -75,6 +75,7 @@ #include "nvim/mouse.h" #include "nvim/event/rstream.h" #include "nvim/event/wstream.h" +#include "nvim/shada.h" static int quitmore = 0; static int ex_pressedreturn = FALSE; @@ -9149,11 +9150,11 @@ static void ex_viminfo(exarg_T *eap) if (*p_viminfo == NUL) p_viminfo = (char_u *)"'100"; if (eap->cmdidx == CMD_rviminfo) { - if (read_viminfo(eap->arg, VIF_WANT_INFO | VIF_WANT_MARKS - | (eap->forceit ? VIF_FORCEIT : 0)) == FAIL) - EMSG(_("E195: Cannot open viminfo file for reading")); - } else - write_viminfo(eap->arg, eap->forceit); + if (shada_read_everything((char *) eap->arg, eap->forceit) == FAIL) + EMSG(_("E195: Cannot open ShaDa file for reading")); + } else { + shada_write_file((char *) eap->arg, eap->forceit); + } p_viminfo = save_viminfo; } diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 9739090d7c..1347feb857 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -65,6 +65,9 @@ #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/event/loop.h" +#include "nvim/os/time.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" /* * Variables shared between getcmdline(), redrawcmdline() and others. @@ -100,12 +103,6 @@ static int cmd_showtail; /* Only show path tail in lists ? */ static int new_cmdpos; /* position set by set_cmdline_pos() */ -typedef struct hist_entry { - int hisnum; /* identifying number */ - int viminfo; /* when TRUE hisstr comes from viminfo */ - char_u *hisstr; /* actual entry, separator char after the NUL */ -} histentry_T; - /* * Type used by call_user_expand_func */ @@ -4230,12 +4227,10 @@ void init_history(void) // delete entries that don't fit in newlen, if any for (int i = 0; i < i1; i++) { - xfree(history[type][i].hisstr); - history[type][i].hisstr = NULL; + hist_free_entry(history[type] + i); } for (int i = i1 + l1; i < i2; i++) { - xfree(history[type][i].hisstr); - history[type][i].hisstr = NULL; + hist_free_entry(history[type] + i); } } @@ -4253,11 +4248,21 @@ void init_history(void) } } -static void clear_hist_entry(histentry_T *hisptr) +static inline void hist_free_entry(histentry_T *hisptr) + FUNC_ATTR_NONNULL_ALL { - hisptr->hisnum = 0; - hisptr->viminfo = FALSE; - hisptr->hisstr = NULL; + xfree(hisptr->hisstr); + if (hisptr->additional_elements != NULL) { + api_free_array(*hisptr->additional_elements); + xfree(hisptr->additional_elements); + } + clear_hist_entry(hisptr); +} + +static inline void clear_hist_entry(histentry_T *hisptr) + FUNC_ATTR_NONNULL_ALL +{ + memset(hisptr, 0, sizeof(*hisptr)); } /* @@ -4310,6 +4315,8 @@ in_history ( history[type][i].hisnum = ++hisnum[type]; history[type][i].viminfo = FALSE; history[type][i].hisstr = str; + history[type][i].timestamp = os_time(); + history[type][i].additional_elements = NULL; return TRUE; } return FALSE; @@ -4372,8 +4379,7 @@ add_to_history ( if (maptick == last_maptick) { /* Current line is from the same mapping, remove it */ hisptr = &history[HIST_SEARCH][hisidx[HIST_SEARCH]]; - xfree(hisptr->hisstr); - clear_hist_entry(hisptr); + hist_free_entry(hisptr); --hisnum[histype]; if (--hisidx[HIST_SEARCH] < 0) hisidx[HIST_SEARCH] = hislen - 1; @@ -4384,11 +4390,13 @@ add_to_history ( if (++hisidx[histype] == hislen) hisidx[histype] = 0; hisptr = &history[histype][hisidx[histype]]; - xfree(hisptr->hisstr); + hist_free_entry(hisptr); /* Store the separator after the NUL of the string. */ len = (int)STRLEN(new_entry); hisptr->hisstr = vim_strnsave(new_entry, len + 2); + hisptr->timestamp = os_time(); + hisptr->additional_elements = NULL; hisptr->hisstr[len + 1] = sep; hisptr->hisnum = ++hisnum[histype]; @@ -4545,23 +4553,21 @@ char_u *get_history_entry(int histype, int idx) return (char_u *)""; } -/* - * Clear all entries of a history. - * "histype" may be one of the HIST_ values. - */ -int clr_history(int histype) +/// Clear all entries in a history +/// +/// @param[in] histype One of the HIST_ values. +/// +/// @return OK if there was something to clean and histype was one of HIST_ +/// values, FAIL otherwise. +int clr_history(const int histype) { - int i; - histentry_T *hisptr; - if (hislen != 0 && histype >= 0 && histype < HIST_COUNT) { - hisptr = history[histype]; - for (i = hislen; i--; ) { - xfree(hisptr->hisstr); - clear_hist_entry(hisptr); + histentry_T *hisptr = history[histype]; + for (int i = hislen; i--; hisptr++) { + hist_free_entry(hisptr); } - hisidx[histype] = -1; /* mark history as cleared */ - hisnum[histype] = 0; /* reset identifier counter */ + hisidx[histype] = -1; // mark history as cleared + hisnum[histype] = 0; // reset identifier counter return OK; } return FAIL; @@ -4596,8 +4602,7 @@ int del_history_entry(int histype, char_u *str) break; if (vim_regexec(®match, hisptr->hisstr, (colnr_T)0)) { found = TRUE; - xfree(hisptr->hisstr); - clear_hist_entry(hisptr); + hist_free_entry(hisptr); } else { if (i != last) { history[histype][last] = *hisptr; @@ -4628,7 +4633,7 @@ int del_history_idx(int histype, int idx) if (i < 0) return FALSE; idx = hisidx[histype]; - xfree(history[histype][i].hisstr); + hist_free_entry(&history[histype][i]); /* When deleting the last added search string in a mapping, reset * last_maptick, so that the last added search string isn't deleted again. @@ -4641,9 +4646,10 @@ int del_history_idx(int histype, int idx) history[histype][i] = history[histype][j]; i = j; } - clear_hist_entry(&history[histype][i]); - if (--i < 0) + clear_hist_entry(&history[histype][idx]); + if (--i < 0) { i += hislen; + } hisidx[histype] = i; return TRUE; } @@ -4763,250 +4769,6 @@ void ex_history(exarg_T *eap) } /* - * Buffers for history read from a viminfo file. Only valid while reading. - */ -static char_u **viminfo_history[HIST_COUNT] = {NULL, NULL, NULL, NULL}; -static int viminfo_hisidx[HIST_COUNT] = {0, 0, 0, 0}; -static int viminfo_hislen[HIST_COUNT] = {0, 0, 0, 0}; -static int viminfo_add_at_front = FALSE; - - -/* - * Translate a history type number to the associated character. - */ -static int -hist_type2char ( - int type, - int use_question /* use '?' instead of '/' */ -) -{ - if (type == HIST_CMD) - return ':'; - if (type == HIST_SEARCH) { - if (use_question) - return '?'; - else - return '/'; - } - if (type == HIST_EXPR) - return '='; - return '@'; -} - -/* - * Prepare for reading the history from the viminfo file. - * This allocates history arrays to store the read history lines. - */ -void prepare_viminfo_history(int asklen, int writing) -{ - int i; - int num; - - init_history(); - viminfo_add_at_front = (asklen != 0 && !writing); - if (asklen > hislen) - asklen = hislen; - - for (int type = 0; type < HIST_COUNT; ++type) { - /* Count the number of empty spaces in the history list. Entries read - * from viminfo previously are also considered empty. If there are - * more spaces available than we request, then fill them up. */ - for (i = 0, num = 0; i < hislen; i++) - if (history[type][i].hisstr == NULL || history[type][i].viminfo) - num++; - int len = asklen; - if (num > len) - len = num; - if (len <= 0) - viminfo_history[type] = NULL; - else - viminfo_history[type] = xmalloc(len * sizeof(char_u *)); - if (viminfo_history[type] == NULL) - len = 0; - viminfo_hislen[type] = len; - viminfo_hisidx[type] = 0; - } -} - -/* - * Accept a line from the viminfo, store it in the history array when it's - * new. - */ -int read_viminfo_history(vir_T *virp, int writing) -{ - int type; - char_u *val; - - type = hist_char2type(virp->vir_line[0]); - if (viminfo_hisidx[type] < viminfo_hislen[type]) { - val = viminfo_readstring(virp, 1, TRUE); - if (val != NULL && *val != NUL) { - int sep = (*val == ' ' ? NUL : *val); - - if (!in_history(type, val + (type == HIST_SEARCH), - viminfo_add_at_front, sep, writing)) { - /* Need to re-allocate to append the separator byte. */ - size_t len = STRLEN(val); - char_u *p = xmalloc(len + 2); - if (type == HIST_SEARCH) { - /* Search entry: Move the separator from the first - * column to after the NUL. */ - memmove(p, val + 1, len); - p[len] = sep; - } else { - /* Not a search entry: No separator in the viminfo - * file, add a NUL separator. */ - memmove(p, val, len + 1); - p[len + 1] = NUL; - } - viminfo_history[type][viminfo_hisidx[type]++] = p; - } - } - xfree(val); - } - return viminfo_readline(virp); -} - -/* - * Finish reading history lines from viminfo. Not used when writing viminfo. - */ -void finish_viminfo_history(void) -{ - int idx; - int i; - int type; - - for (type = 0; type < HIST_COUNT; ++type) { - if (history[type] == NULL) - continue; - idx = hisidx[type] + viminfo_hisidx[type]; - if (idx >= hislen) - idx -= hislen; - else if (idx < 0) - idx = hislen - 1; - if (viminfo_add_at_front) - hisidx[type] = idx; - else { - if (hisidx[type] == -1) - hisidx[type] = hislen - 1; - do { - if (history[type][idx].hisstr != NULL - || history[type][idx].viminfo) - break; - if (++idx == hislen) - idx = 0; - } while (idx != hisidx[type]); - if (idx != hisidx[type] && --idx < 0) - idx = hislen - 1; - } - for (i = 0; i < viminfo_hisidx[type]; i++) { - xfree(history[type][idx].hisstr); - history[type][idx].hisstr = viminfo_history[type][i]; - history[type][idx].viminfo = TRUE; - if (--idx < 0) - idx = hislen - 1; - } - idx += 1; - idx %= hislen; - for (i = 0; i < viminfo_hisidx[type]; i++) { - history[type][idx++].hisnum = ++hisnum[type]; - idx %= hislen; - } - xfree(viminfo_history[type]); - viminfo_history[type] = NULL; - viminfo_hisidx[type] = 0; - } -} - -/* - * Write history to viminfo file in "fp". - * When "merge" is TRUE merge history lines with a previously read viminfo - * file, data is in viminfo_history[]. - * When "merge" is FALSE just write all history lines. Used for ":wviminfo!". - */ -void write_viminfo_history(FILE *fp, int merge) -{ - int i; - int type; - int num_saved; - char_u *p; - int c; - int round; - - init_history(); - if (hislen == 0) - return; - for (type = 0; type < HIST_COUNT; ++type) { - num_saved = get_viminfo_parameter(hist_type2char(type, FALSE)); - if (num_saved == 0) - continue; - if (num_saved < 0) /* Use default */ - num_saved = hislen; - fprintf(fp, _("\n# %s History (newest to oldest):\n"), - type == HIST_CMD ? _("Command Line") : - type == HIST_SEARCH ? _("Search String") : - type == HIST_EXPR ? _("Expression") : - _("Input Line")); - if (num_saved > hislen) - num_saved = hislen; - - /* - * Merge typed and viminfo history: - * round 1: history of typed commands. - * round 2: history from recently read viminfo. - */ - for (round = 1; round <= 2; ++round) { - if (round == 1) - /* start at newest entry, somewhere in the list */ - i = hisidx[type]; - else if (viminfo_hisidx[type] > 0) - /* start at newest entry, first in the list */ - i = 0; - else - /* empty list */ - i = -1; - if (i >= 0) - while (num_saved > 0 - && !(round == 2 && i >= viminfo_hisidx[type])) { - p = round == 1 ? history[type][i].hisstr - : viminfo_history[type] == NULL ? NULL - : viminfo_history[type][i]; - if (p != NULL && (round == 2 - || !merge - || !history[type][i].viminfo)) { - --num_saved; - fputc(hist_type2char(type, TRUE), fp); - /* For the search history: put the separator in the - * second column; use a space if there isn't one. */ - if (type == HIST_SEARCH) { - c = p[STRLEN(p) + 1]; - putc(c == NUL ? ' ' : c, fp); - } - viminfo_writestring(fp, p); - } - if (round == 1) { - /* Decrement index, loop around and stop when back at - * the start. */ - if (--i < 0) - i = hislen - 1; - if (i == hisidx[type]) - break; - } else { - /* Increment index. Stop at the end in the while. */ - ++i; - } - } - } - for (i = 0; i < viminfo_hisidx[type]; ++i) - if (viminfo_history[type] != NULL) - xfree(viminfo_history[type][i]); - xfree(viminfo_history[type]); - viminfo_history[type] = NULL; - viminfo_hisidx[type] = 0; - } -} - -/* * Write a character at the current cursor+offset position. * It is directly written into the command buffer block. */ @@ -5294,3 +5056,68 @@ char_u *script_get(exarg_T *eap, char_u *cmd) return (char_u *)ga.ga_data; } + +/// Iterate over history items +/// +/// @warning No history-editing functions must be run while iteration is in +/// progress. +/// +/// @param[in] iter Pointer to the last history entry. +/// @param[in] history_type Type of the history (HIST_*). Ignored if iter +/// parameter is not NULL. +/// @param[in] zero If true then zero (but not free) returned items. +/// +/// @warning When using this parameter user is +/// responsible for calling clr_history() +/// itself after iteration is over. If +/// clr_history() is not called behaviour is +/// undefined. No functions that work with +/// history must be called during iteration +/// in this case. +/// @param[out] hist Next history entry. +/// +/// @return Pointer used in next iteration or NULL to indicate that iteration +/// was finished. +const void *hist_iter(const void *const iter, const size_t history_type, + const bool zero, histentry_T *const hist) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(4) +{ + *hist = (histentry_T) { + .hisstr = NULL + }; + if (hisidx[history_type] == -1) { + return NULL; + } + histentry_T *const hstart = &(history[history_type][0]); + histentry_T *const hlast = ( + &(history[history_type][hisidx[history_type]])); + const histentry_T *const hend = &(history[history_type][hislen - 1]); + histentry_T *hiter; + if (iter == NULL) { + histentry_T *hfirst = hlast; + do { + hfirst++; + if (hfirst > hend) { + hfirst = hstart; + } + if (hfirst->hisstr != NULL) { + break; + } + } while (hfirst != hlast); + hiter = hfirst; + } else { + hiter = (histentry_T *) iter; + } + if (hiter == NULL) { + return NULL; + } + *hist = *hiter; + if (zero) { + memset(hiter, 0, sizeof(*hiter)); + } + if (hiter == hlast) { + return NULL; + } + hiter++; + return (const void *) ((hiter > hend) ? hstart : hiter); +} diff --git a/src/nvim/ex_getln.h b/src/nvim/ex_getln.h index 2b82f934d5..9c7a688e7e 100644 --- a/src/nvim/ex_getln.h +++ b/src/nvim/ex_getln.h @@ -35,6 +35,15 @@ typedef char_u *(*CompleteListItemGetter)(expand_T *, int); +/// History entry definition +typedef struct hist_entry { + int hisnum; ///< Entry identifier number. + bool viminfo; ///< If true, indicates that entry comes from viminfo. + char_u *hisstr; ///< Actual entry, separator char after the NUL. + Timestamp timestamp; ///< Time when entry was added. + Array *additional_elements; ///< Additional entries from ShaDa file. +} histentry_T; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ex_getln.h.generated.h" #endif diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 302f6b30fb..e4b59b7316 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -57,6 +57,7 @@ #include "nvim/types.h" #include "nvim/undo.h" #include "nvim/window.h" +#include "nvim/shada.h" #include "nvim/os/os.h" #include "nvim/os/time.h" #include "nvim/os/input.h" @@ -2166,14 +2167,15 @@ readfile_charconvert ( /* - * Read marks for the current buffer from the viminfo file, when we support + * Read marks for the current buffer from the ShaDa file, when we support * buffer marks and the buffer has a name. */ static void check_marks_read(void) { if (!curbuf->b_marks_read && get_viminfo_parameter('\'') > 0 - && curbuf->b_ffname != NULL) - read_viminfo(NULL, VIF_WANT_MARKS); + && curbuf->b_ffname != NULL) { + shada_read_marks(); + } /* Always set b_marks_read; needed when 'viminfo' is changed to include * the ' parameter after opening a buffer. */ diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 60d03cec0c..4ad0d2ec90 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -892,7 +892,7 @@ EXTERN int skip_redraw INIT(= FALSE); /* skip redraw once */ EXTERN int do_redraw INIT(= FALSE); /* extra redraw once */ EXTERN int need_highlight_changed INIT(= TRUE); -EXTERN char_u *use_viminfo INIT(= NULL); /* name of viminfo file to use */ +EXTERN char *used_shada_file INIT(= NULL); /* name of viminfo file to use */ #define NSCRIPT 15 EXTERN FILE *scriptin[NSCRIPT]; /* streams to read script from */ diff --git a/src/nvim/main.c b/src/nvim/main.c index 27f8340ec7..0e25d6d4ec 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -49,6 +49,7 @@ #include "nvim/ops.h" #include "nvim/option.h" #include "nvim/os_unix.h" +#include "nvim/os/os_defs.h" #include "nvim/path.h" #include "nvim/profile.h" #include "nvim/quickfix.h" @@ -58,6 +59,7 @@ #include "nvim/ui.h" #include "nvim/version.h" #include "nvim/window.h" +#include "nvim/shada.h" #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/os/time.h" @@ -377,12 +379,12 @@ int main(int argc, char **argv) } /* - * Read in registers, history etc, but not marks, from the viminfo file. + * Read in registers, history etc, but not marks, from the ShaDa file. * This is where v:oldfiles gets filled. */ if (*p_viminfo != NUL) { - read_viminfo(NULL, VIF_WANT_INFO | VIF_GET_OLDFILES); - TIME_MSG("reading viminfo"); + (void) shada_read_file(NULL, kShaDaWantInfo|kShaDaGetOldfiles); + TIME_MSG("reading ShaDa"); } /* It's better to make v:oldfiles an empty list than NULL. */ if (get_vim_var_list(VV_OLDFILES) == NULL) @@ -803,9 +805,10 @@ void getout(int exitval) apply_autocmds(EVENT_VIMLEAVEPRE, NULL, NULL, FALSE, curbuf); } - if (p_viminfo && *p_viminfo != NUL) - /* Write out the registers, history, marks etc, to the viminfo file */ - write_viminfo(NULL, FALSE); + if (p_viminfo && *p_viminfo != NUL) { + // Write out the registers, history, marks etc, to the viminfo file + shada_write_file(NULL, false); + } if (get_vim_var_nr(VV_DYING) <= 1) apply_autocmds(EVENT_VIMLEAVE, NULL, NULL, FALSE, curbuf); @@ -1164,7 +1167,7 @@ static void command_line_scan(mparm_T *parmp) } /*FALLTHROUGH*/ case 'S': /* "-S {file}" execute Vim script */ - case 'i': /* "-i {viminfo}" use for viminfo */ + case 'i': /* "-i {shada}" use for ShaDa file */ case 'u': /* "-u {vimrc}" vim inits file */ case 'U': /* "-U {gvimrc}" gvim inits file */ case 'W': /* "-W {scriptout}" overwrite */ @@ -1235,8 +1238,8 @@ static void command_line_scan(mparm_T *parmp) parmp->use_ef = (char_u *)argv[0]; break; - case 'i': /* "-i {viminfo}" use for viminfo */ - use_viminfo = (char_u *)argv[0]; + case 'i': /* "-i {shada}" use for shada */ + used_shada_file = argv[0]; break; case 's': /* "-s {scriptin}" read from script file */ @@ -2039,7 +2042,7 @@ static void usage(void) mch_msg(_(" -r, -L List swap files and exit\n")); mch_msg(_(" -r <file> Recover crashed session\n")); mch_msg(_(" -u <nvimrc> Use <nvimrc> instead of the default\n")); - mch_msg(_(" -i <nviminfo> Use <nviminfo> instead of the default\n")); + mch_msg(_(" -i <shada> Use <shada> instead of the default " SHADA_FILE "\n")); mch_msg(_(" --noplugin Don't load plugin scripts\n")); mch_msg(_(" -o[N] Open N windows (default: one for each file)\n")); mch_msg(_(" -O[N] Like -o but split vertically\n")); diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 6ac4b1de9c..7fdf4f2bcb 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -39,7 +39,10 @@ #include "nvim/strings.h" #include "nvim/ui.h" #include "nvim/os/os.h" +#include "nvim/os/time.h" #include "nvim/os/input.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" /* * This file contains routines to maintain and manipulate marks. @@ -48,12 +51,20 @@ /* * If a named file mark's lnum is non-zero, it is valid. * If a named file mark's fnum is non-zero, it is for an existing buffer, - * otherwise it is from .viminfo and namedfm[n].fname is the file name. + * otherwise it is from .shada and namedfm[n].fname is the file name. * There are marks 'A - 'Z (set by user) and '0 to '9 (set when writing - * viminfo). + * shada). */ -#define EXTRA_MARKS 10 /* marks 0-9 */ -static xfmark_T namedfm[NMARKS + EXTRA_MARKS]; /* marks with file nr */ + +/// Global marks (marks with file number or name) +static xfmark_T namedfm[NGLOBALMARKS]; + +#define SET_XFMARK(xfmarkp_, mark_, fnum_, fname_) \ + do { \ + xfmark_T *const xfmarkp__ = xfmarkp_; \ + xfmarkp__->fname = fname_; \ + RESET_FMARK(&(xfmarkp__->fmark), mark_, fnum_); \ + } while (0) #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -68,6 +79,23 @@ int setmark(int c) return setmark_pos(c, &curwin->w_cursor, curbuf->b_fnum); } +/// Free fmark_T item +void free_fmark(fmark_T fm) +{ + if (fm.additional_data != NULL) { + api_free_dictionary(*fm.additional_data); + free(fm.additional_data); + } +} + +/// Free xfmark_T item +static inline void free_xfmark(xfmark_T fm) +{ + xfree(fm.fname); + fm.fname = NULL; + free_fmark(fm.fmark); +} + /* * Set named mark "c" to position "pos". * When "c" is upper case use file "fnum". @@ -92,7 +120,7 @@ int setmark_pos(int c, pos_T *pos, int fnum) } if (c == '"') { - curbuf->b_last_cursor = *pos; + RESET_FMARK(&curbuf->b_last_cursor, *pos, curbuf->b_fnum); return OK; } @@ -123,16 +151,14 @@ int setmark_pos(int c, pos_T *pos, int fnum) return FAIL; if (islower(c)) { i = c - 'a'; - curbuf->b_namedm[i] = *pos; + RESET_FMARK(curbuf->b_namedm + i, *pos, curbuf->b_fnum); return OK; } if (isupper(c)) { assert(c >= 'A' && c <= 'Z'); i = c - 'A'; - namedfm[i].fmark.mark = *pos; - namedfm[i].fmark.fnum = fnum; - xfree(namedfm[i].fname); - namedfm[i].fname = NULL; + free_xfmark(namedfm[i]); + SET_XFMARK(namedfm + i, *pos, fnum, NULL); return OK; } return FAIL; @@ -157,16 +183,14 @@ void setpcmark(void) /* If jumplist is full: remove oldest entry */ if (++curwin->w_jumplistlen > JUMPLISTSIZE) { curwin->w_jumplistlen = JUMPLISTSIZE; - xfree(curwin->w_jumplist[0].fname); + free_xfmark(curwin->w_jumplist[0]); for (i = 1; i < JUMPLISTSIZE; ++i) curwin->w_jumplist[i - 1] = curwin->w_jumplist[i]; } curwin->w_jumplistidx = curwin->w_jumplistlen; fm = &curwin->w_jumplist[curwin->w_jumplistlen - 1]; - fm->fmark.mark = curwin->w_pcmark; - fm->fmark.fnum = curbuf->b_fnum; - fm->fname = NULL; + SET_XFMARK(fm, curwin->w_pcmark, curbuf->b_fnum, NULL); } /* @@ -302,11 +326,11 @@ pos_T *getmark_buf_fnum(buf_T *buf, int c, int changefile, int *fnum) pos_copy = curwin->w_pcmark; /* need to make a copy because */ posp = &pos_copy; /* w_pcmark may be changed soon */ } else if (c == '"') /* to pos when leaving buffer */ - posp = &(buf->b_last_cursor); + posp = &(buf->b_last_cursor.mark); else if (c == '^') /* to where Insert mode stopped */ - posp = &(buf->b_last_insert); + posp = &(buf->b_last_insert.mark); else if (c == '.') /* to where last change was made */ - posp = &(buf->b_last_change); + posp = &(buf->b_last_change.mark); else if (c == '[') /* to start of previous operator */ posp = &(buf->b_op_start); else if (c == ']') /* to end of previous operator */ @@ -357,7 +381,7 @@ pos_T *getmark_buf_fnum(buf_T *buf, int c, int changefile, int *fnum) pos_copy.coladd = 0; } } else if (ASCII_ISLOWER(c)) { /* normal named mark */ - posp = &(buf->b_namedm[c - 'a']); + posp = &(buf->b_namedm[c - 'a'].mark); } else if (ASCII_ISUPPER(c) || ascii_isdigit(c)) { /* named file mark */ if (ascii_isdigit(c)) c = c - '0' + NMARKS; @@ -365,8 +389,9 @@ pos_T *getmark_buf_fnum(buf_T *buf, int c, int changefile, int *fnum) c -= 'A'; posp = &(namedfm[c].fmark.mark); - if (namedfm[c].fmark.fnum == 0) + if (namedfm[c].fmark.fnum == 0) { fname2fnum(&namedfm[c]); + } if (fnum != NULL) *fnum = namedfm[c].fmark.fnum; @@ -420,15 +445,15 @@ getnextmark ( pos.col = MAXCOL; for (i = 0; i < NMARKS; i++) { - if (curbuf->b_namedm[i].lnum > 0) { + if (curbuf->b_namedm[i].mark.lnum > 0) { if (dir == FORWARD) { - if ((result == NULL || lt(curbuf->b_namedm[i], *result)) - && lt(pos, curbuf->b_namedm[i])) - result = &curbuf->b_namedm[i]; + if ((result == NULL || lt(curbuf->b_namedm[i].mark, *result)) + && lt(pos, curbuf->b_namedm[i].mark)) + result = &curbuf->b_namedm[i].mark; } else { - if ((result == NULL || lt(*result, curbuf->b_namedm[i])) - && lt(curbuf->b_namedm[i], pos)) - result = &curbuf->b_namedm[i]; + if ((result == NULL || lt(*result, curbuf->b_namedm[i].mark)) + && lt(curbuf->b_namedm[i].mark, pos)) + result = &curbuf->b_namedm[i].mark; } } } @@ -438,12 +463,12 @@ getnextmark ( /* * For an xtended filemark: set the fnum from the fname. - * This is used for marks obtained from the .viminfo file. It's postponed + * This is used for marks obtained from the .shada file. It's postponed * until the mark is used to avoid a long startup delay. */ static void fname2fnum(xfmark_T *fm) { - char_u *p; + char_u *p; if (fm->fname != NULL) { /* @@ -475,19 +500,17 @@ static void fname2fnum(xfmark_T *fm) /* * Check all file marks for a name that matches the file name in buf. * May replace the name with an fnum. - * Used for marks that come from the .viminfo file. + * Used for marks that come from the .shada file. */ void fmarks_check_names(buf_T *buf) { - char_u *name; + char_u *name = buf->b_ffname; int i; if (buf->b_ffname == NULL) return; - name = home_replace_save(buf, buf->b_ffname); - - for (i = 0; i < NMARKS + EXTRA_MARKS; ++i) + for (i = 0; i < NGLOBALMARKS; ++i) fmarks_check_one(&namedfm[i], name, buf); FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { @@ -495,8 +518,6 @@ void fmarks_check_names(buf_T *buf) fmarks_check_one(&wp->w_jumplist[i], name, buf); } } - - xfree(name); } static void fmarks_check_one(xfmark_T *fm, char_u *name, buf_T *buf) @@ -541,23 +562,20 @@ int check_mark(pos_T *pos) */ void clrallmarks(buf_T *buf) { - static int i = -1; + static bool initialized = false; - if (i == -1) /* first call ever: initialize */ - for (i = 0; i < NMARKS + 1; i++) { - namedfm[i].fmark.mark.lnum = 0; - namedfm[i].fname = NULL; - } + if (!initialized) { + // first call ever: initialize + memset(&(namedfm[0]), 0, sizeof(namedfm)); + initialized = true; + } - for (i = 0; i < NMARKS; i++) - buf->b_namedm[i].lnum = 0; + memset(&(buf->b_namedm[0]), 0, sizeof(buf->b_namedm)); buf->b_op_start.lnum = 0; /* start/end op mark cleared */ buf->b_op_end.lnum = 0; - buf->b_last_cursor.lnum = 1; /* '" mark cleared */ - buf->b_last_cursor.col = 0; - buf->b_last_cursor.coladd = 0; - buf->b_last_insert.lnum = 0; /* '^ mark cleared */ - buf->b_last_change.lnum = 0; /* '. mark cleared */ + RESET_FMARK(&buf->b_last_cursor, ((pos_T) {1, 0, 0}), 0); // '" mark + CLEAR_FMARK(&buf->b_last_insert); // '^ mark + CLEAR_FMARK(&buf->b_last_change); // '. mark buf->b_changelistlen = 0; } @@ -612,8 +630,8 @@ void do_marks(exarg_T *eap) show_one_mark('\'', arg, &curwin->w_pcmark, NULL, TRUE); for (i = 0; i < NMARKS; ++i) - show_one_mark(i + 'a', arg, &curbuf->b_namedm[i], NULL, TRUE); - for (i = 0; i < NMARKS + EXTRA_MARKS; ++i) { + show_one_mark(i + 'a', arg, &curbuf->b_namedm[i].mark, NULL, TRUE); + for (i = 0; i < NGLOBALMARKS; ++i) { if (namedfm[i].fmark.fnum != 0) name = fm_getname(&namedfm[i].fmark, 15); else @@ -626,11 +644,11 @@ void do_marks(exarg_T *eap) xfree(name); } } - show_one_mark('"', arg, &curbuf->b_last_cursor, NULL, TRUE); + show_one_mark('"', arg, &curbuf->b_last_cursor.mark, NULL, TRUE); show_one_mark('[', arg, &curbuf->b_op_start, NULL, TRUE); show_one_mark(']', arg, &curbuf->b_op_end, NULL, TRUE); - show_one_mark('^', arg, &curbuf->b_last_insert, NULL, TRUE); - show_one_mark('.', arg, &curbuf->b_last_change, NULL, TRUE); + show_one_mark('^', arg, &curbuf->b_last_insert.mark, NULL, TRUE); + show_one_mark('.', arg, &curbuf->b_last_change.mark, NULL, TRUE); show_one_mark('<', arg, &curbuf->b_visual.vi_start, NULL, TRUE); show_one_mark('>', arg, &curbuf->b_visual.vi_end, NULL, TRUE); show_one_mark(-1, arg, NULL, NULL, FALSE); @@ -728,7 +746,7 @@ void ex_delmarks(exarg_T *eap) for (i = from; i <= to; ++i) { if (lower) - curbuf->b_namedm[i - 'a'].lnum = 0; + curbuf->b_namedm[i - 'a'].mark.lnum = 0; else { if (digit) n = i - '0' + NMARKS; @@ -741,9 +759,9 @@ void ex_delmarks(exarg_T *eap) } } else switch (*p) { - case '"': curbuf->b_last_cursor.lnum = 0; break; - case '^': curbuf->b_last_insert.lnum = 0; break; - case '.': curbuf->b_last_change.lnum = 0; break; + case '"': CLEAR_FMARK(&curbuf->b_last_cursor); break; + case '^': CLEAR_FMARK(&curbuf->b_last_insert); break; + case '.': CLEAR_FMARK(&curbuf->b_last_change); break; case '[': curbuf->b_op_start.lnum = 0; break; case ']': curbuf->b_op_end.lnum = 0; break; case '<': curbuf->b_visual.vi_start.lnum = 0; break; @@ -886,24 +904,24 @@ void mark_adjust(linenr_T line1, linenr_T line2, long amount, long amount_after) if (!cmdmod.lockmarks) { /* named marks, lower case and upper case */ for (i = 0; i < NMARKS; i++) { - one_adjust(&(curbuf->b_namedm[i].lnum)); + one_adjust(&(curbuf->b_namedm[i].mark.lnum)); if (namedfm[i].fmark.fnum == fnum) one_adjust_nodel(&(namedfm[i].fmark.mark.lnum)); } - for (i = NMARKS; i < NMARKS + EXTRA_MARKS; i++) { + for (i = NMARKS; i < NGLOBALMARKS; i++) { if (namedfm[i].fmark.fnum == fnum) one_adjust_nodel(&(namedfm[i].fmark.mark.lnum)); } /* last Insert position */ - one_adjust(&(curbuf->b_last_insert.lnum)); + one_adjust(&(curbuf->b_last_insert.mark.lnum)); /* last change position */ - one_adjust(&(curbuf->b_last_change.lnum)); + one_adjust(&(curbuf->b_last_change.mark.lnum)); /* last cursor position, if it was set */ - if (!equalpos(curbuf->b_last_cursor, initpos)) - one_adjust(&(curbuf->b_last_cursor.lnum)); + if (!equalpos(curbuf->b_last_cursor.mark, initpos)) + one_adjust(&(curbuf->b_last_cursor.mark.lnum)); /* list of change positions */ @@ -1038,20 +1056,20 @@ void mark_col_adjust(linenr_T lnum, colnr_T mincol, long lnum_amount, long col_a /* named marks, lower case and upper case */ for (i = 0; i < NMARKS; i++) { - col_adjust(&(curbuf->b_namedm[i])); + col_adjust(&(curbuf->b_namedm[i].mark)); if (namedfm[i].fmark.fnum == fnum) col_adjust(&(namedfm[i].fmark.mark)); } - for (i = NMARKS; i < NMARKS + EXTRA_MARKS; i++) { + for (i = NMARKS; i < NGLOBALMARKS; i++) { if (namedfm[i].fmark.fnum == fnum) col_adjust(&(namedfm[i].fmark.mark)); } /* last Insert position */ - col_adjust(&(curbuf->b_last_insert)); + col_adjust(&(curbuf->b_last_insert.mark)); /* last change position */ - col_adjust(&(curbuf->b_last_change)); + col_adjust(&(curbuf->b_last_change.mark)); /* list of change positions */ for (i = 0; i < curbuf->b_changelistlen; ++i) @@ -1150,382 +1168,274 @@ void copy_jumplist(win_T *from, win_T *to) to->w_jumplistidx = from->w_jumplistidx; } -/* - * Free items in the jumplist of window "wp". - */ -void free_jumplist(win_T *wp) -{ - int i; - - for (i = 0; i < wp->w_jumplistlen; ++i) - xfree(wp->w_jumplist[i].fname); -} - -void set_last_cursor(win_T *win) +/// Iterate over jumplist items +/// +/// @warning No jumplist-editing functions must be run while iteration is in +/// progress. +/// +/// @param[in] iter Iterator. Pass NULL to start iteration. +/// @param[in] win Window for which jump list is processed. +/// @param[out] fm Item definition. +/// +/// @return Pointer that needs to be passed to next `mark_jumplist_iter` call or +/// NULL if iteration is over. +const void *mark_jumplist_iter(const void *const iter, const win_T *const win, + xfmark_T *const fm) + FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ARG(2, 3) FUNC_ATTR_WARN_UNUSED_RESULT { - if (win->w_buffer != NULL) - win->w_buffer->b_last_cursor = win->w_cursor; + if (iter == NULL && win->w_jumplistlen == 0) { + *fm = (xfmark_T) {{{0, 0, 0}, 0, 0, NULL}, NULL}; + return NULL; + } + const xfmark_T *const iter_mark = + (iter == NULL + ? &(win->w_jumplist[win->w_jumplistlen - 1]) + : (const xfmark_T *const) iter); + *fm = *iter_mark; + if (iter_mark == &(win->w_jumplist[0])) { + return NULL; + } else { + return iter_mark - 1; + } } -#if defined(EXITFREE) -void free_all_marks(void) +/// Iterate over global marks +/// +/// @warning No mark-editing functions must be run while iteration is in +/// progress. +/// +/// @param[in] iter Iterator. Pass NULL to start iteration. +/// @param[out] name Mark name. +/// @param[out] fm Mark definition. +/// +/// @return Pointer that needs to be passed to next `mark_global_iter` call or +/// NULL if iteration is over. +const void *mark_global_iter(const void *const iter, char *const name, + xfmark_T *const fm) + FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ARG(2, 3) FUNC_ATTR_WARN_UNUSED_RESULT { - int i; - - for (i = 0; i < NMARKS + EXTRA_MARKS; i++) - if (namedfm[i].fmark.mark.lnum != 0) - xfree(namedfm[i].fname); + const xfmark_T *iter_mark = (iter == NULL + ? &(namedfm[0]) + : (const xfmark_T *const) iter); + while (!iter_mark->fmark.mark.lnum + && (size_t) (iter_mark - &(namedfm[0])) < ARRAY_SIZE(namedfm)) { + iter_mark++; + } + if (!iter_mark->fmark.mark.lnum) { + *fm = (xfmark_T) {.fmark = {.mark = {.lnum = 0}}}; + } + size_t iter_off = (size_t) (iter_mark - &(namedfm[0])); + *name = (char) (iter_off < NMARKS + ? 'A' + (char) iter_off + : '0' + (char) (iter_off - NMARKS)); + *fm = *iter_mark; + while ((size_t) (++iter_mark - &(namedfm[0])) < ARRAY_SIZE(namedfm)) { + if (iter_mark->fmark.mark.lnum) { + return (const void *) iter_mark; + } + } + return NULL; } -#endif - -int read_viminfo_filemark(vir_T *virp, int force) +/// Get next mark and its name +/// +/// @param[in] buf Buffer for which next mark is taken. +/// @param[in,out] mark_name Pointer to the current mark name. Next mark name +/// will be saved at this address as well. +/// +/// Current mark name must either be NUL, '"', '^', +/// '.' or 'a' .. 'z'. If it is neither of these +/// behaviour is undefined. +/// +/// @return Pointer to the next mark or NULL. +static inline const fmark_T *next_buffer_mark(const buf_T *const buf, + char *const mark_name) + FUNC_ATTR_NONNULL_ALL { - char_u *str; - xfmark_T *fm; - int i; - - /* We only get here if line[0] == '\'' or '-'. - * Illegal mark names are ignored (for future expansion). */ - str = virp->vir_line + 1; - if ( - *str <= 127 && - ((*virp->vir_line == '\'' && (ascii_isdigit(*str) || isupper(*str))) - || (*virp->vir_line == '-' && *str == '\''))) { - if (*str == '\'') { - /* If the jumplist isn't full insert fmark as oldest entry */ - if (curwin->w_jumplistlen == JUMPLISTSIZE) - fm = NULL; - else { - for (i = curwin->w_jumplistlen; i > 0; --i) - curwin->w_jumplist[i] = curwin->w_jumplist[i - 1]; - ++curwin->w_jumplistidx; - ++curwin->w_jumplistlen; - fm = &curwin->w_jumplist[0]; - fm->fmark.mark.lnum = 0; - fm->fname = NULL; - } - } else if (ascii_isdigit(*str)) - fm = &namedfm[*str - '0' + NMARKS]; - else { // is uppercase - assert(*str >= 'A' && *str <= 'Z'); - fm = &namedfm[*str - 'A']; + switch (*mark_name) { + case NUL: { + *mark_name = '"'; + return &(buf->b_last_cursor); + } + case '"': { + *mark_name = '^'; + return &(buf->b_last_insert); + } + case '^': { + *mark_name = '.'; + return &(buf->b_last_change); } - if (fm != NULL && (fm->fmark.mark.lnum == 0 || force)) { - str = skipwhite(str + 1); - fm->fmark.mark.lnum = getdigits_long(&str); - str = skipwhite(str); - fm->fmark.mark.col = getdigits_int(&str); - fm->fmark.mark.coladd = 0; - fm->fmark.fnum = 0; - str = skipwhite(str); - xfree(fm->fname); - fm->fname = viminfo_readstring(virp, (int)(str - virp->vir_line), - FALSE); + case '.': { + *mark_name = 'a'; + return &(buf->b_namedm[0]); + } + case 'z': { + return NULL; + } + default: { + (*mark_name)++; + return &(buf->b_namedm[*mark_name - 'a']); } } - return vim_fgets(virp->vir_line, LSIZE, virp->vir_fd); } -void write_viminfo_filemarks(FILE *fp) +/// Iterate over buffer marks +/// +/// @warning No mark-editing functions must be run while iteration is in +/// progress. +/// +/// @param[in] iter Iterator. Pass NULL to start iteration. +/// @param[in] buf Buffer. +/// @param[out] name Mark name. +/// @param[out] fm Mark definition. +/// +/// @return Pointer that needs to be passed to next `mark_buffer_iter` call or +/// NULL if iteration is over. +const void *mark_buffer_iter(const void *const iter, const buf_T *const buf, + char *const name, fmark_T *const fm) + FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ARG(2, 3, 4) FUNC_ATTR_WARN_UNUSED_RESULT { - int i; - char_u *name; - buf_T *buf; - xfmark_T *fm; - - if (get_viminfo_parameter('f') == 0) - return; - - fputs(_("\n# File marks:\n"), fp); - - /* - * Find a mark that is the same file and position as the cursor. - * That one, or else the last one is deleted. - * Move '0 to '1, '1 to '2, etc. until the matching one or '9 - * Set '0 mark to current cursor position. - */ - if (curbuf->b_ffname != NULL && !removable(curbuf->b_ffname)) { - name = buflist_nr2name(curbuf->b_fnum, TRUE, FALSE); - for (i = NMARKS; i < NMARKS + EXTRA_MARKS - 1; ++i) - if (namedfm[i].fmark.mark.lnum == curwin->w_cursor.lnum - && (namedfm[i].fname == NULL - ? namedfm[i].fmark.fnum == curbuf->b_fnum - : (name != NULL - && STRCMP(name, namedfm[i].fname) == 0))) - break; - xfree(name); - - xfree(namedfm[i].fname); - for (; i > NMARKS; --i) - namedfm[i] = namedfm[i - 1]; - namedfm[NMARKS].fmark.mark = curwin->w_cursor; - namedfm[NMARKS].fmark.fnum = curbuf->b_fnum; - namedfm[NMARKS].fname = NULL; + char mark_name = (char) (iter == NULL + ? NUL + : (iter == &(buf->b_last_cursor) + ? '"' + : (iter == &(buf->b_last_insert) + ? '^' + : (iter == &(buf->b_last_change) + ? '.' + : 'a' + (char) ((const fmark_T *)iter + - &(buf->b_namedm[0])))))); + const fmark_T *iter_mark = next_buffer_mark(buf, &mark_name); + while (iter_mark != NULL && iter_mark->mark.lnum == 0) { + iter_mark = next_buffer_mark(buf, &mark_name); } - - /* Write the filemarks '0 - '9 and 'A - 'Z */ - for (i = 0; i < NMARKS + EXTRA_MARKS; i++) - write_one_filemark(fp, &namedfm[i], '\'', - i < NMARKS ? i + 'A' : i - NMARKS + '0'); - - /* Write the jumplist with -' */ - fputs(_("\n# Jumplist (newest first):\n"), fp); - setpcmark(); /* add current cursor position */ - cleanup_jumplist(); - for (fm = &curwin->w_jumplist[curwin->w_jumplistlen - 1]; - fm >= &curwin->w_jumplist[0]; --fm) { - if (fm->fmark.fnum == 0 - || ((buf = buflist_findnr(fm->fmark.fnum)) != NULL - && !removable(buf->b_ffname))) - write_one_filemark(fp, fm, '-', '\''); + if (iter_mark == NULL) { + *fm = (fmark_T) {.mark = {.lnum = 0}}; + return NULL; + } + size_t iter_off = (size_t) (iter_mark - &(buf->b_namedm[0])); + if (mark_name) { + *name = mark_name; + } else { + *name = (char) ('a' + (char) iter_off); } + *fm = *iter_mark; + return (const void *) iter_mark; } -static void write_one_filemark(FILE *fp, xfmark_T *fm, int c1, int c2) +/// Get a number of valid marks +size_t mark_global_amount(void) + FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - char_u *name; - - if (fm->fmark.mark.lnum == 0) /* not set */ - return; - - if (fm->fmark.fnum != 0) /* there is a buffer */ - name = buflist_nr2name(fm->fmark.fnum, TRUE, FALSE); - else - name = fm->fname; /* use name from .viminfo */ - if (name != NULL && *name != NUL) { - fprintf(fp, "%c%c %" PRId64 " %" PRId64 " ", - c1, c2, (int64_t)fm->fmark.mark.lnum, (int64_t)fm->fmark.mark.col); - viminfo_writestring(fp, name); + size_t ret = 0; + for (size_t i = 0; i < NGLOBALMARKS; i++) { + if (namedfm[i].fmark.mark.lnum != 0) { + ret++; + } } - - if (fm->fmark.fnum != 0) - xfree(name); + return ret; } -/* - * Return TRUE if "name" is on removable media (depending on 'viminfo'). - */ -int removable(char_u *name) +/// Get a number of valid marks +size_t mark_buffer_amount(const buf_T *const buf) + FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL { - char_u *p; - char_u part[51]; - int retval = FALSE; - size_t n; - - name = home_replace_save(NULL, name); - for (p = p_viminfo; *p; ) { - copy_option_part(&p, part, 51, ", "); - if (part[0] == 'r') { - n = STRLEN(part + 1); - if (mb_strnicmp(part + 1, name, n) == 0) { - retval = TRUE; - break; - } + size_t ret = (size_t) ((buf->b_last_cursor.mark.lnum != 0) + + (buf->b_last_insert.mark.lnum != 0) + + (buf->b_last_change.mark.lnum != 0)); + for (size_t i = 0; i < NMARKS; i++) { + if (buf->b_namedm[i].mark.lnum != 0) { + ret++; } } - xfree(name); - return retval; + return ret; } - -/* - * Write all the named marks for all buffers. - * Return the number of buffers for which marks have been written. - */ -int write_viminfo_marks(FILE *fp_out) +/// Set global mark +/// +/// @param[in] name Mark name. +/// @param[in] fm Mark to be set. +/// @param[in] update If true then only set global mark if it was created +/// later then existing one. +void mark_set_global(const char name, const xfmark_T fm, const bool update) { - /* - * Set b_last_cursor for the all buffers that have a window. - */ - FOR_ALL_TAB_WINDOWS(tp, win) { - set_last_cursor(win); + xfmark_T *fm_tgt = NULL; + if (ASCII_ISUPPER(name)) { + fm_tgt = &(namedfm[name - 'A']); + } else if (ascii_isdigit(name)) { + fm_tgt = &(namedfm[NMARKS + (name - '0')]); + } else { + return; } - - fputs(_("\n# History of marks within files (newest to oldest):\n"), fp_out); - int count = 0; - FOR_ALL_BUFFERS(buf) { - /* - * Only write something if buffer has been loaded and at least one - * mark is set. - */ - if (buf->b_marks_read) { - bool is_mark_set = true; - if (buf->b_last_cursor.lnum == 0) { - is_mark_set = false; - for (int i = 0; i < NMARKS; i++) { - if (buf->b_namedm[i].lnum != 0) { - is_mark_set = true; - break; - } - } - } - if (is_mark_set && buf->b_ffname != NULL - && buf->b_ffname[0] != NUL && !removable(buf->b_ffname)) { - home_replace(NULL, buf->b_ffname, IObuff, IOSIZE, TRUE); - fprintf(fp_out, "\n> "); - viminfo_writestring(fp_out, IObuff); - write_one_mark(fp_out, '"', &buf->b_last_cursor); - write_one_mark(fp_out, '^', &buf->b_last_insert); - write_one_mark(fp_out, '.', &buf->b_last_change); - /* changelist positions are stored oldest first */ - for (int i = 0; i < buf->b_changelistlen; ++i) { - write_one_mark(fp_out, '+', &buf->b_changelist[i]); - } - for (int i = 0; i < NMARKS; i++) { - write_one_mark(fp_out, 'a' + i, &buf->b_namedm[i]); - } - count++; - } - } + if (update && fm.fmark.timestamp < fm_tgt->fmark.timestamp) { + return; } - - return count; + if (fm_tgt->fmark.mark.lnum != 0) { + free_xfmark(*fm_tgt); + } + *fm_tgt = fm; } -static void write_one_mark(FILE *fp_out, int c, pos_T *pos) +/// Set local mark +/// +/// @param[in] name Mark name. +/// @param[in] buf Pointer to the buffer to set mark in. +/// @param[in] fm Mark to be set. +/// @param[in] update If true then only set global mark if it was created +/// later then existing one. +void mark_set_local(const char name, buf_T *const buf, + const fmark_T fm, const bool update) + FUNC_ATTR_NONNULL_ALL { - if (pos->lnum != 0) - fprintf(fp_out, "\t%c\t%" PRId64 "\t%d\n", c, - (int64_t)pos->lnum, (int)pos->col); + fmark_T *fm_tgt = NULL; + if (ASCII_ISLOWER(name)) { + fm_tgt = &(buf->b_namedm[name - 'a']); + } else if (name == '"') { + fm_tgt = &(buf->b_last_cursor); + } else if (name == '^') { + fm_tgt = &(buf->b_last_insert); + } else if (name == '.') { + fm_tgt = &(buf->b_last_change); + } else { + return; + } + if (update && fm.timestamp < fm_tgt->timestamp) { + return; + } + if (fm_tgt->mark.lnum != 0) { + free_fmark(*fm_tgt); + } + *fm_tgt = fm; } /* - * Handle marks in the viminfo file: - * fp_out != NULL: copy marks for buffers not in buffer list - * fp_out == NULL && (flags & VIF_WANT_MARKS): read marks for curbuf only - * fp_out == NULL && (flags & VIF_GET_OLDFILES | VIF_FORCEIT): fill v:oldfiles + * Free items in the jumplist of window "wp". */ -void copy_viminfo_marks(vir_T *virp, FILE *fp_out, int count, int eof, int flags) +void free_jumplist(win_T *wp) { - char_u *line = virp->vir_line; - buf_T *buf; - int num_marked_files; - int load_marks; - int copy_marks_out; - char_u *str; int i; - char_u *p; - char_u *name_buf; - pos_T pos; - list_T *list = NULL; - name_buf = xmalloc(LSIZE); - *name_buf = NUL; - - if (fp_out == NULL && (flags & (VIF_GET_OLDFILES | VIF_FORCEIT))) { - list = list_alloc(); - set_vim_var_list(VV_OLDFILES, list); + for (i = 0; i < wp->w_jumplistlen; ++i) { + free_xfmark(wp->w_jumplist[i]); } +} - num_marked_files = get_viminfo_parameter('\''); - while (!eof && (count < num_marked_files || fp_out == NULL)) { - if (line[0] != '>') { - if (line[0] != '\n' && line[0] != '\r' && line[0] != '#') { - if (viminfo_error("E576: ", _("Missing '>'"), line)) - break; /* too many errors, return now */ - } - eof = vim_fgets(line, LSIZE, virp->vir_fd); - continue; /* Skip this dud line */ - } - - /* - * Handle long line and translate escaped characters. - * Find file name, set str to start. - * Ignore leading and trailing white space. - */ - str = skipwhite(line + 1); - str = viminfo_readstring(virp, (int)(str - virp->vir_line), FALSE); - p = str + STRLEN(str); - while (p != str && (*p == NUL || ascii_isspace(*p))) - p--; - if (*p) - p++; - *p = NUL; - - if (list != NULL) - list_append_string(list, str, -1); +void set_last_cursor(win_T *win) +{ + if (win->w_buffer != NULL) { + RESET_FMARK(&win->w_buffer->b_last_cursor, win->w_cursor, 0); + } +} - /* - * If fp_out == NULL, load marks for current buffer. - * If fp_out != NULL, copy marks for buffers not in buflist. - */ - load_marks = copy_marks_out = FALSE; - if (fp_out == NULL) { - if ((flags & VIF_WANT_MARKS) && curbuf->b_ffname != NULL) { - if (*name_buf == NUL) /* only need to do this once */ - home_replace(NULL, curbuf->b_ffname, name_buf, LSIZE, TRUE); - if (fnamecmp(str, name_buf) == 0) - load_marks = TRUE; - } - } else { /* fp_out != NULL */ - /* This is slow if there are many buffers!! */ - buf = NULL; - FOR_ALL_BUFFERS(bp) { - if (bp->b_ffname != NULL) { - home_replace(NULL, bp->b_ffname, name_buf, LSIZE, TRUE); - if (fnamecmp(str, name_buf) == 0) { - buf = bp; - break; - } - } - } +#if defined(EXITFREE) +void free_all_marks(void) +{ + int i; - /* - * copy marks if the buffer has not been loaded - */ - if (buf == NULL || !buf->b_marks_read) { - copy_marks_out = TRUE; - fputs("\n> ", fp_out); - viminfo_writestring(fp_out, str); - count++; - } - } - xfree(str); - - pos.coladd = 0; - while (!(eof = viminfo_readline(virp)) && line[0] == TAB) { - if (load_marks) { - if (line[1] != NUL) { - int64_t lnum_64; - unsigned int u; - sscanf((char *)line + 2, "%" SCNd64 "%u", &lnum_64, &u); - // safely downcast to linenr_T (long); remove when linenr_T refactored - assert(lnum_64 <= LONG_MAX); - pos.lnum = (linenr_T)lnum_64; - assert(u <= INT_MAX); - pos.col = (colnr_T)u; - switch (line[1]) { - case '"': curbuf->b_last_cursor = pos; break; - case '^': curbuf->b_last_insert = pos; break; - case '.': curbuf->b_last_change = pos; break; - case '+': - /* changelist positions are stored oldest - * first */ - if (curbuf->b_changelistlen == JUMPLISTSIZE) - /* list is full, remove oldest entry */ - memmove(curbuf->b_changelist, - curbuf->b_changelist + 1, - sizeof(pos_T) * (JUMPLISTSIZE - 1)); - else - ++curbuf->b_changelistlen; - curbuf->b_changelist[ - curbuf->b_changelistlen - 1] = pos; - break; - default: if ((i = line[1] - 'a') >= 0 && i < NMARKS) - curbuf->b_namedm[i] = pos; - } - } - } else if (copy_marks_out) - fputs((char *)line, fp_out); - } - if (load_marks) { - FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { - if (wp->w_buffer == curbuf) - wp->w_changelistidx = curbuf->b_changelistlen; - } - break; + for (i = 0; i < NGLOBALMARKS; i++) { + if (namedfm[i].fmark.mark.lnum != 0) { + free_xfmark(namedfm[i]); } } - xfree(name_buf); } +#endif diff --git a/src/nvim/mark.h b/src/nvim/mark.h index aa89a5b625..a8aaf56648 100644 --- a/src/nvim/mark.h +++ b/src/nvim/mark.h @@ -4,6 +4,22 @@ #include "nvim/buffer_defs.h" #include "nvim/mark_defs.h" #include "nvim/pos.h" +#include "nvim/os/time.h" + +/// Free and set fmark using given value +#define RESET_FMARK(fmarkp_, mark_, fnum_) \ + do { \ + fmark_T *const fmarkp__ = fmarkp_; \ + free_fmark(*fmarkp__); \ + fmarkp__->mark = mark_; \ + fmarkp__->fnum = fnum_; \ + fmarkp__->timestamp = os_time(); \ + fmarkp__->additional_data = NULL; \ + } while (0) + +/// Clear given fmark +#define CLEAR_FMARK(fmarkp_) \ + RESET_FMARK(fmarkp_, ((pos_T) {0, 0, 0}), 0) #ifdef INCLUDE_GENERATED_DECLARATIONS # include "mark.h.generated.h" diff --git a/src/nvim/mark_defs.h b/src/nvim/mark_defs.h index 67392234d3..2f7c9b4ed4 100644 --- a/src/nvim/mark_defs.h +++ b/src/nvim/mark_defs.h @@ -2,25 +2,41 @@ #define NVIM_MARK_DEFS_H #include "nvim/pos.h" +#include "nvim/os/time.h" +#include "nvim/api/private/defs.h" /* * marks: positions in a file * (a normal mark is a lnum/col pair, the same as a file position) */ -#define NMARKS ('z' - 'a' + 1) /* max. # of named marks */ -#define JUMPLISTSIZE 100 /* max. # of marks in jump list */ -#define TAGSTACKSIZE 20 /* max. # of tags in tag stack */ +/// Number of possible numbered global marks +#define EXTRA_MARKS ('9' - '0' + 1) +/// Maximum possible number of letter marks +#define NMARKS ('z' - 'a' + 1) + +/// Total possible number of global marks +#define NGLOBALMARKS (NMARKS + EXTRA_MARKS) + +/// Maximum number of marks in jump list +#define JUMPLISTSIZE 100 + +/// Maximum number of tags in tag stack +#define TAGSTACKSIZE 20 + +/// Structure defining single local mark typedef struct filemark { - pos_T mark; /* cursor position */ - int fnum; /* file number */ + pos_T mark; ///< Cursor position. + int fnum; ///< File number. + Timestamp timestamp; ///< Time when this mark was last set. + Dictionary *additional_data; ///< Additional data from ShaDa file. } fmark_T; -/* Xtended file mark: also has a file name */ +/// Structure defining extended mark (mark with file name attached) typedef struct xfilemark { - fmark_T fmark; - char_u *fname; /* file name, used when fnum == 0 */ + fmark_T fmark; ///< Actual mark. + char_u *fname; ///< File name, used when fnum == 0. } xfmark_T; #endif // NVIM_MARK_DEFS_H diff --git a/src/nvim/mbyte.c b/src/nvim/mbyte.c index 100b66a608..087d2e677c 100644 --- a/src/nvim/mbyte.c +++ b/src/nvim/mbyte.c @@ -66,7 +66,7 @@ * (4) The encoding of the file is specified with 'fileencoding'. Conversion * is to be done when it's different from 'encoding'. * - * The viminfo file is a special case: Only text is converted, not file names. + * The ShaDa file is a special case: Only text is converted, not file names. * Vim scripts may contain an ":encoding" command. This has an effect for * some commands, like ":menutrans" */ diff --git a/src/nvim/misc1.c b/src/nvim/misc1.c index 8407198b13..a4ebdca091 100644 --- a/src/nvim/misc1.c +++ b/src/nvim/misc1.c @@ -2042,8 +2042,7 @@ static void changed_common(linenr_T lnum, colnr_T col, linenr_T lnume, long xtra /* set the '. mark */ if (!cmdmod.keepjumps) { - curbuf->b_last_change.lnum = lnum; - curbuf->b_last_change.col = col; + RESET_FMARK(&curbuf->b_last_change, ((pos_T) {lnum, col, 0}), 0); /* Create a new entry if a new undo-able change was started or we * don't have an entry yet. */ @@ -2095,7 +2094,7 @@ static void changed_common(linenr_T lnum, colnr_T col, linenr_T lnume, long xtra } } curbuf->b_changelist[curbuf->b_changelistlen - 1] = - curbuf->b_last_change; + curbuf->b_last_change.mark; /* The current window is always after the last change, so that "g," * takes you back to it. */ curwin->w_changelistidx = curbuf->b_changelistlen; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 467b74f9e6..5354fb20ad 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -6327,8 +6327,8 @@ static void nv_g_cmd(cmdarg_T *cap) * "gi": start Insert at the last position. */ case 'i': - if (curbuf->b_last_insert.lnum != 0) { - curwin->w_cursor = curbuf->b_last_insert; + if (curbuf->b_last_insert.mark.lnum != 0) { + curwin->w_cursor = curbuf->b_last_insert.mark; check_cursor_lnum(); i = (int)STRLEN(get_cursor_line_ptr()); if (curwin->w_cursor.col > (colnr_T)i) { diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 3163132b8a..55c7aa3364 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -50,6 +50,9 @@ #include "nvim/undo.h" #include "nvim/window.h" #include "nvim/os/input.h" +#include "nvim/os/time.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" /* * Registers: @@ -62,14 +65,14 @@ */ #define DELETION_REGISTER 36 #define NUM_SAVED_REGISTERS 37 -// The following registers should not be saved in viminfo: +// The following registers should not be saved in ShaDa file: #define STAR_REGISTER 37 #define PLUS_REGISTER 38 #define NUM_REGISTERS 39 static yankreg_T y_regs[NUM_REGISTERS]; -static yankreg_T *y_previous = NULL; /* ptr to last written yankreg */ +static yankreg_T *y_previous = NULL; /* ptr to last written yankreg */ static bool clipboard_didwarn_unnamed = false; @@ -746,6 +749,31 @@ typedef enum { YREG_PUT, } yreg_mode_t; +/// Convert register name into register index +/// +/// @param[in] regname Register name. +/// +/// @return Index in y_regs array or -1 if register name was not recognized. +static inline int reg_index(const int regname) + FUNC_ATTR_CONST +{ + if (ascii_isdigit(regname)) { + return regname - '0'; + } else if (ASCII_ISLOWER(regname)) { + return CharOrdLow(regname) + 10; + } else if (ASCII_ISUPPER(regname)) { + return CharOrdUp(regname) + 10; + } else if (regname == '-') { + return DELETION_REGISTER; + } else if (regname == '*') { + return STAR_REGISTER; + } else if (regname == '+') { + return PLUS_REGISTER; + } else { + return -1; + } +} + /// Return yankreg_T to use, according to the value of `regname`. /// Cannot handle the '_' (black hole) register. /// Must only be called with a valid register name! @@ -778,19 +806,11 @@ yankreg_T *get_yank_register(int regname, int mode) return y_previous; } - int i = 0; // when not 0-9, a-z, A-Z or '-'/'+'/'*': use register 0 - if (ascii_isdigit(regname)) - i = regname - '0'; - else if (ASCII_ISLOWER(regname)) - i = CharOrdLow(regname) + 10; - else if (ASCII_ISUPPER(regname)) { - i = CharOrdUp(regname) + 10; - } else if (regname == '-') - i = DELETION_REGISTER; - else if (regname == '*') - i = STAR_REGISTER; - else if (regname == '+') - i = PLUS_REGISTER; + int i = reg_index(regname); + // when not 0-9, a-z, A-Z or '-'/'+'/'*': use register 0 + if (i == -1) { + i = 0; + } reg = &y_regs[i]; if (mode == YREG_YANK) { @@ -890,6 +910,20 @@ int do_record(int c) return retval; } +static void set_yreg_additional_data(yankreg_T *reg, + Dictionary *additional_data) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (reg->additional_data == additional_data) { + return; + } + if (reg->additional_data != NULL) { + api_free_dictionary(*reg->additional_data); + free(reg->additional_data); + } + reg->additional_data = additional_data; +} + /* * Stuff string "p" into yank register "regname" as a single line (append if * uppercase). "p" must have been alloced. @@ -919,11 +953,13 @@ static int stuff_yank(int regname, char_u *p) *pp = lp; } else { free_register(reg); + set_yreg_additional_data(reg, NULL); reg->y_array = (char_u **)xmalloc(sizeof(char_u *)); reg->y_array[0] = p; reg->y_size = 1; reg->y_type = MCHAR; /* used to be MLINE, why? */ } + reg->timestamp = os_time(); return OK; } @@ -2266,10 +2302,7 @@ int op_change(oparg_T *oap) */ void init_yank(void) { - int i; - - for (i = 0; i < NUM_REGISTERS; i++) - y_regs[i].y_array = NULL; + memset(&(y_regs[0]), 0, sizeof(y_regs)); } #if defined(EXITFREE) @@ -2291,6 +2324,7 @@ void clear_registers(void) void free_register(yankreg_T *reg) FUNC_ATTR_NONNULL_ALL { + set_yreg_additional_data(reg, NULL); if (reg->y_array != NULL) { long i; @@ -2369,6 +2403,8 @@ static void op_yank_reg(oparg_T *oap, bool message, yankreg_T *reg, bool append) reg->y_type = yanktype; /* set the yank register type */ reg->y_width = 0; reg->y_array = xcalloc(yanklines, sizeof(char_u *)); + set_yreg_additional_data(reg, NULL); + reg->timestamp = os_time(); y_idx = 0; lnum = oap->start.lnum; @@ -4433,171 +4469,6 @@ int do_addsub(int command, linenr_T Prenum1) return OK; } -int read_viminfo_register(vir_T *virp, int force) -{ - int eof; - int do_it = TRUE; - int size; - int limit; - int set_prev = FALSE; - char_u *str; - char_u **array = NULL; - - /* We only get here (hopefully) if line[0] == '"' */ - str = virp->vir_line + 1; - - /* If the line starts with "" this is the y_previous register. */ - if (*str == '"') { - set_prev = TRUE; - str++; - } - - if (!ASCII_ISALNUM(*str) && *str != '-') { - if (viminfo_error("E577: ", _("Illegal register name"), virp->vir_line)) - return TRUE; /* too many errors, pretend end-of-file */ - do_it = FALSE; - } - yankreg_T *reg = get_yank_register(*str++, YREG_PUT); - if (!force && reg->y_array != NULL) - do_it = FALSE; - - if (*str == '@') { - /* "x@: register x used for @@ */ - if (force || execreg_lastc == NUL) - execreg_lastc = str[-1]; - } - - size = 0; - limit = 100; /* Optimized for registers containing <= 100 lines */ - if (do_it) { - if (set_prev) { - y_previous = reg; - } - - free_register(reg); - array = xmalloc(limit * sizeof(char_u *)); - - str = skipwhite(skiptowhite(str)); - if (STRNCMP(str, "CHAR", 4) == 0) { - reg->y_type = MCHAR; - } else if (STRNCMP(str, "BLOCK", 5) == 0) { - reg->y_type = MBLOCK; - } else { - reg->y_type = MLINE; - } - /* get the block width; if it's missing we get a zero, which is OK */ - str = skipwhite(skiptowhite(str)); - reg->y_width = getdigits_int(&str); - } - - while (!(eof = viminfo_readline(virp)) - && (virp->vir_line[0] == TAB || virp->vir_line[0] == '<')) { - if (do_it) { - if (size >= limit) { - limit *= 2; - array = xrealloc(array, limit * sizeof(char_u *)); - } - array[size++] = viminfo_readstring(virp, 1, TRUE); - } - } - - if (do_it) { - if (size == 0) { - xfree(array); - } else if (size < limit) { - reg->y_array = xrealloc(array, size * sizeof(char_u *)); - } else { - reg->y_array = array; - } - reg->y_size = size; - } - return eof; -} - -void write_viminfo_registers(FILE *fp) -{ - int i, j; - char_u *type; - char_u c; - int num_lines; - int max_num_lines; - int max_kbyte; - long len; - - fputs(_("\n# Registers:\n"), fp); - - /* Get '<' value, use old '"' value if '<' is not found. */ - max_num_lines = get_viminfo_parameter('<'); - if (max_num_lines < 0) - max_num_lines = get_viminfo_parameter('"'); - if (max_num_lines == 0) - return; - max_kbyte = get_viminfo_parameter('s'); - if (max_kbyte == 0) - return; - - // don't include clipboard registers '*'/'+' - for (i = 0; i < NUM_SAVED_REGISTERS; i++) { - if (y_regs[i].y_array == NULL) - continue; - - /* Skip empty registers. */ - num_lines = y_regs[i].y_size; - if (num_lines == 0 - || (num_lines == 1 && y_regs[i].y_type == MCHAR - && *y_regs[i].y_array[0] == NUL)) - continue; - - if (max_kbyte > 0) { - /* Skip register if there is more text than the maximum size. */ - len = 0; - for (j = 0; j < num_lines; j++) - len += (long)STRLEN(y_regs[i].y_array[j]) + 1L; - if (len > (long)max_kbyte * 1024L) - continue; - } - - switch (y_regs[i].y_type) { - case MLINE: - type = (char_u *)"LINE"; - break; - case MCHAR: - type = (char_u *)"CHAR"; - break; - case MBLOCK: - type = (char_u *)"BLOCK"; - break; - default: - sprintf((char *)IObuff, _("E574: Unknown register type %d"), - y_regs[i].y_type); - emsg(IObuff); - type = (char_u *)"LINE"; - break; - } - if (y_previous == &y_regs[i]) - fprintf(fp, "\""); - c = get_register_name(i); - fprintf(fp, "\"%c", c); - if (c == execreg_lastc) - fprintf(fp, "@"); - fprintf(fp, "\t%s\t%d\n", type, - (int)y_regs[i].y_width - ); - - /* If max_num_lines < 0, then we save ALL the lines in the register */ - if (max_num_lines > 0 && num_lines > max_num_lines) - num_lines = max_num_lines; - for (j = 0; j < num_lines; j++) { - putc('\t', fp); - viminfo_writestring(fp, y_regs[i].y_array[j]); - } - } -} - - - - - /* * Return the type of a register. * Used for getregtype() @@ -4739,7 +4610,6 @@ void *get_reg_contents(int regname, int flags) return retval; } - static yankreg_T *init_write_reg(int name, yankreg_T **old_y_previous, bool must_append) { if (!valid_yank_reg(name, true)) { // check for valid reg name @@ -4973,6 +4843,8 @@ static void str_to_reg(yankreg_T *y_ptr, int yank_type, const char_u *str, } y_ptr->y_type = type; y_ptr->y_size = lnum; + set_yreg_additional_data(y_ptr, NULL); + y_ptr->timestamp = os_time(); if (type == MBLOCK) { y_ptr->y_width = (blocklen == -1 ? (colnr_T) maxlen - 1 : blocklen); } else { @@ -5363,6 +5235,10 @@ static bool get_clipboard(int name, yankreg_T **target, bool quiet) reg->y_array = xcalloc(lines->lv_len, sizeof(uint8_t *)); reg->y_size = lines->lv_len; + reg->additional_data = NULL; + reg->timestamp = 0; + // Timestamp is not saved for clipboard registers because clipboard registers + // are not saved in the viminfo. int i = 0; for (listitem_T *li = lines->lv_first; li != NULL; li = li->li_next) { @@ -5411,6 +5287,8 @@ err: } reg->y_array = NULL; reg->y_size = 0; + reg->additional_data = NULL; + reg->timestamp = 0; if (errmsg) { EMSG("clipboard: provider returned invalid data"); } @@ -5478,3 +5356,70 @@ void end_global_changes(void) clipboard_needs_update = false; } } + +/// Check whether register is empty +static inline bool reg_empty(const yankreg_T *const reg) + FUNC_ATTR_CONST +{ + return (reg->y_array == NULL + || reg->y_size == 0 + || (reg->y_size == 1 + && reg->y_type == MCHAR + && *(reg->y_array[0]) == NUL)); +} + +/// Iterate over registerrs +/// +/// @param[in] iter Iterator. Pass NULL to start iteration. +/// @param[out] name Register name. +/// @param[out] reg Register contents. +/// +/// @return Pointer that needs to be passed to next `op_register_iter` call or +/// NULL if iteration is over. +const void *op_register_iter(const void *const iter, char *const name, + yankreg_T *const reg) + FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ARG(2, 3) FUNC_ATTR_WARN_UNUSED_RESULT +{ + const yankreg_T *iter_reg = (iter == NULL + ? &(y_regs[0]) + : (const yankreg_T *const) iter); + while (reg_empty(iter_reg) && iter_reg - &(y_regs[0]) < NUM_SAVED_REGISTERS) { + iter_reg++; + } + if (reg_empty(iter_reg)) { + *reg = (yankreg_T) {.y_array = NULL}; + return NULL; + } + size_t iter_off = iter_reg - &(y_regs[0]); + *name = (char) get_register_name(iter_off); + *reg = *iter_reg; + while (++iter_reg - &(y_regs[0]) < NUM_SAVED_REGISTERS) { + if (!reg_empty(iter_reg)) { + return (void *) iter_reg; + } + } + return NULL; +} + +/// Get a number of non-empty registers +size_t op_register_amount(void) + FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT +{ + size_t ret = 0; + for (size_t i = 0; i < NUM_SAVED_REGISTERS; i++) { + if (!reg_empty(y_regs + i)) { + ret++; + } + } + return ret; +} + +/// Set register to a given value +void register_set(const char name, const yankreg_T reg) +{ + int i = reg_index(name); + if (i == -1) { + return; + } + y_regs[i] = reg; +} diff --git a/src/nvim/ops.h b/src/nvim/ops.h index 99683165f9..4da5cfc93d 100644 --- a/src/nvim/ops.h +++ b/src/nvim/ops.h @@ -4,6 +4,8 @@ #include <stdbool.h> #include "nvim/types.h" +#include "nvim/api/private/defs.h" +#include "nvim/os/time.h" typedef int (*Indenter)(void); @@ -47,14 +49,6 @@ typedef int (*Indenter)(void); #define OP_FORMAT2 26 /* "gw" format operator, keeps cursor pos */ #define OP_FUNCTION 27 /* "g@" call 'operatorfunc' */ -/// Contents of a yank (read-write) register -typedef struct yankreg { - char_u **y_array; ///< pointer to array of line pointers - linenr_T y_size; ///< number of lines in y_array - char_u y_type; ///< MLINE, MCHAR or MBLOCK - colnr_T y_width; ///< only set if y_type == MBLOCK -} yankreg_T; - /// Flags for get_reg_contents(). enum GRegFlags { kGRegNoExpr = 1, ///< Do not allow expression register. @@ -62,6 +56,16 @@ enum GRegFlags { kGRegList = 4 ///< Return list. }; +/// Definition of one register +typedef struct yankreg { + char_u **y_array; ///< Pointer to an array of line pointers. + linenr_T y_size; ///< Number of lines in y_array. + char_u y_type; ///< Register type: MLINE, MCHAR or MBLOCK. + colnr_T y_width; ///< Register width (only valid for y_type == MBLOCK). + Timestamp timestamp; ///< Time when register was last modified. + Dictionary *additional_data; ///< Additional data from ShaDa file. +} yankreg_T; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ops.h.generated.h" #endif diff --git a/src/nvim/os/time.c b/src/nvim/os/time.c index ee17938afc..ba1dcf631a 100644 --- a/src/nvim/os/time.c +++ b/src/nvim/os/time.c @@ -103,3 +103,12 @@ struct tm *os_get_localtime(struct tm *result) FUNC_ATTR_NONNULL_ALL time_t rawtime = time(NULL); return os_localtime_r(&rawtime, result); } + +/// Obtains the current UNIX timestamp +/// +/// @return Seconds since epoch. +Timestamp os_time(void) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + return (Timestamp) time(NULL); +} diff --git a/src/nvim/os/time.h b/src/nvim/os/time.h index b21808307f..3a6b665b51 100644 --- a/src/nvim/os/time.h +++ b/src/nvim/os/time.h @@ -5,6 +5,8 @@ #include <stdbool.h> #include <time.h> +typedef time_t Timestamp; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "os/time.h.generated.h" #endif diff --git a/src/nvim/os/unix_defs.h b/src/nvim/os/unix_defs.h index 9ab4ba1c1a..949973bf40 100644 --- a/src/nvim/os/unix_defs.h +++ b/src/nvim/os/unix_defs.h @@ -43,8 +43,8 @@ #ifndef VIMRC_FILE # define VIMRC_FILE ".nvimrc" #endif -#ifndef VIMINFO_FILE -# define VIMINFO_FILE "~/.nviminfo" +#ifndef SHADA_FILE +# define SHADA_FILE "~/.nvim/shada/main.shada" #endif // Default for 'backupdir'. diff --git a/src/nvim/os/win_defs.h b/src/nvim/os/win_defs.h index 9773b73428..b7ec50a109 100644 --- a/src/nvim/os/win_defs.h +++ b/src/nvim/os/win_defs.h @@ -9,7 +9,7 @@ // Defines needed to fix the build on Windows: // - USR_EXRC_FILE // - USR_VIMRC_FILE -// - VIMINFO_FILE +// - SHADA_FILE // - DFLT_DIR // - DFLT_BDIR // - DFLT_VDIR diff --git a/src/nvim/search.c b/src/nvim/search.c index a758e02105..f8dd7bd482 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -51,6 +51,7 @@ #include "nvim/ui.h" #include "nvim/window.h" #include "nvim/os/time.h" +#include "nvim/api/private/helpers.h" #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -79,23 +80,6 @@ * Henry Spencer's regular expression library. See regexp.c. */ -/* The offset for a search command is store in a soff struct */ -/* Note: only spats[0].off is really used */ -struct soffset { - int dir; /* search direction, '/' or '?' */ - int line; /* search has line offset */ - int end; /* search set cursor at end */ - long off; /* line or char offset */ -}; - -/* A search pattern and its attributes are stored in a spat struct */ -struct spat { - char_u *pat; /* the pattern (in allocated memory) or NULL */ - int magic; /* magicness of the pattern */ - int no_scs; /* no smartcase for this pattern */ - struct soffset off; -}; - /* * Two search patterns are remembered: One for the :substitute command and * one for other searches. last_idx points to the one that was used the last @@ -103,8 +87,10 @@ struct spat { */ static struct spat spats[2] = { - {NULL, TRUE, FALSE, {'/', 0, 0, 0L}}, /* last used search pat */ - {NULL, TRUE, FALSE, {'/', 0, 0, 0L}} /* last used substitute pat */ + // Last used search pattern + [0] = {NULL, true, false, 0, {'/', false, false, 0L}, NULL}, + // Last used substitute pattern + [1] = {NULL, true, false, 0, {'/', false, false, 0L}, NULL} }; static int last_idx = 0; /* index in spats[] for RE_LAST */ @@ -256,10 +242,12 @@ char_u *reverse_text(char_u *s) FUNC_ATTR_NONNULL_RET void save_re_pat(int idx, char_u *pat, int magic) { if (spats[idx].pat != pat) { - xfree(spats[idx].pat); + free_spat(&spats[idx]); spats[idx].pat = vim_strsave(pat); spats[idx].magic = magic; spats[idx].no_scs = no_smartcase; + spats[idx].timestamp = os_time(); + spats[idx].additional_data = NULL; last_idx = idx; /* If 'hlsearch' set and search pat changed: need redraw. */ if (p_hls) @@ -291,21 +279,30 @@ void save_search_patterns(void) void restore_search_patterns(void) { if (--save_level == 0) { - xfree(spats[0].pat); + free_spat(&spats[0]); spats[0] = saved_spats[0]; set_vv_searchforward(); - xfree(spats[1].pat); + free_spat(&spats[1]); spats[1] = saved_spats[1]; last_idx = saved_last_idx; SET_NO_HLSEARCH(saved_no_hlsearch); } } +static inline void free_spat(struct spat *const spat) +{ + xfree(spat->pat); + if (spat->additional_data != NULL) { + api_free_dictionary(*spat->additional_data); + xfree(spat->additional_data); + } +} + #if defined(EXITFREE) void free_search_patterns(void) { - xfree(spats[0].pat); - xfree(spats[1].pat); + free_spat(&spats[0]); + free_spat(&spats[1]); if (mr_pattern_alloced) { xfree(mr_pattern); @@ -414,17 +411,19 @@ void reset_search_dir(void) } /* - * Set the last search pattern. For ":let @/ =" and viminfo. + * Set the last search pattern. For ":let @/ =" and ShaDa file. * Also set the saved search pattern, so that this works in an autocommand. */ void set_last_search_pat(const char_u *s, int idx, int magic, int setlast) { - xfree(spats[idx].pat); + free_spat(&spats[idx]); /* An empty string means that nothing should be matched. */ if (*s == NUL) spats[idx].pat = NULL; else spats[idx].pat = (char_u *) xstrdup((char *) s); + spats[idx].timestamp = os_time(); + spats[idx].additional_data = NULL; spats[idx].magic = magic; spats[idx].no_scs = FALSE; spats[idx].off.dir = '/'; @@ -435,7 +434,7 @@ void set_last_search_pat(const char_u *s, int idx, int magic, int setlast) if (setlast) last_idx = idx; if (save_level) { - xfree(saved_spats[idx].pat); + free_spat(&saved_spats[idx]); saved_spats[idx] = spats[0]; if (spats[idx].pat == NULL) saved_spats[idx].pat = NULL; @@ -1053,7 +1052,7 @@ int do_search( else if ((options & SEARCH_OPT) && (*p == 'e' || *p == 's' || *p == 'b')) { if (*p == 'e') /* end */ - spats[0].off.end = SEARCH_END; + spats[0].off.end = true; ++p; } if (ascii_isdigit(*p) || *p == '+' || *p == '-') { /* got an offset */ @@ -1166,12 +1165,13 @@ int do_search( lrFswap(searchstr,0); c = searchit(curwin, curbuf, &pos, dirc == '/' ? FORWARD : BACKWARD, - searchstr, count, spats[0].off.end + (options & - (SEARCH_KEEP + SEARCH_PEEK + - SEARCH_HIS - + SEARCH_MSG + SEARCH_START - + ((pat != NULL && *pat == - ';') ? 0 : SEARCH_NOOF))), + searchstr, count, (spats[0].off.end * SEARCH_END + + (options & + (SEARCH_KEEP + SEARCH_PEEK + + SEARCH_HIS + + SEARCH_MSG + SEARCH_START + + ((pat != NULL && *pat == + ';') ? 0 : SEARCH_NOOF)))), RE_LAST, (linenr_T)0, tm); if (dircp != NULL) @@ -4605,105 +4605,45 @@ static void show_pat_in_path(char_u *line, int type, int did_show, int action, F } } -int read_viminfo_search_pattern(vir_T *virp, int force) +/// Get last search pattern +void get_search_pattern(SearchPattern *const pat) { - char_u *lp; - int idx = -1; - int magic = FALSE; - int no_scs = FALSE; - int off_line = FALSE; - int off_end = 0; - long off = 0; - int setlast = FALSE; - static int hlsearch_on = FALSE; - char_u *val; + memcpy(pat, &(spats[0]), sizeof(spats[0])); +} - /* - * Old line types: - * "/pat", "&pat": search/subst. pat - * "~/pat", "~&pat": last used search/subst. pat - * New line types: - * "~h", "~H": hlsearch highlighting off/on - * "~<magic><smartcase><line><end><off><last><which>pat" - * <magic>: 'm' off, 'M' on - * <smartcase>: 's' off, 'S' on - * <line>: 'L' line offset, 'l' char offset - * <end>: 'E' from end, 'e' from start - * <off>: decimal, offset - * <last>: '~' last used pattern - * <which>: '/' search pat, '&' subst. pat - */ - lp = virp->vir_line; - if (lp[0] == '~' && (lp[1] == 'm' || lp[1] == 'M')) { /* new line type */ - if (lp[1] == 'M') /* magic on */ - magic = TRUE; - if (lp[2] == 's') - no_scs = TRUE; - if (lp[3] == 'L') - off_line = TRUE; - if (lp[4] == 'E') - off_end = SEARCH_END; - lp += 5; - off = getdigits_long(&lp); - } - if (lp[0] == '~') { /* use this pattern for last-used pattern */ - setlast = TRUE; - lp++; - } - if (lp[0] == '/') - idx = RE_SEARCH; - else if (lp[0] == '&') - idx = RE_SUBST; - else if (lp[0] == 'h') /* ~h: 'hlsearch' highlighting off */ - hlsearch_on = FALSE; - else if (lp[0] == 'H') /* ~H: 'hlsearch' highlighting on */ - hlsearch_on = TRUE; - if (idx >= 0) { - if (force || spats[idx].pat == NULL) { - val = viminfo_readstring(virp, (int)(lp - virp->vir_line + 1), TRUE); - set_last_search_pat(val, idx, magic, setlast); - xfree(val); - spats[idx].no_scs = no_scs; - spats[idx].off.line = off_line; - spats[idx].off.end = off_end; - spats[idx].off.off = off; - if (setlast) { - SET_NO_HLSEARCH(!hlsearch_on); - } - } - } - return viminfo_readline(virp); +/// Get last substitute pattern +void get_substitute_pattern(SearchPattern *const pat) +{ + memcpy(pat, &(spats[1]), sizeof(spats[1])); + memset(&(pat->off), 0, sizeof(pat->off)); } -void write_viminfo_search_pattern(FILE *fp) +/// Set last search pattern +void set_search_pattern(const SearchPattern pat) { - if (get_viminfo_parameter('/') != 0) { - fprintf(fp, "\n# hlsearch on (H) or off (h):\n~%c", - (no_hlsearch || find_viminfo_parameter('h') != NULL) ? 'h' : 'H'); - wvsp_one(fp, RE_SEARCH, "", '/'); - wvsp_one(fp, RE_SUBST, _("Substitute "), '&'); - } + free_spat(&spats[0]); + memcpy(&(spats[0]), &pat, sizeof(spats[0])); } -static void -wvsp_one ( - FILE *fp, /* file to write to */ - int idx, /* spats[] index */ - char *s, /* search pat */ - int sc /* dir char */ -) +/// Set last substitute pattern +void set_substitute_pattern(const SearchPattern pat) { - if (spats[idx].pat != NULL) { - fprintf(fp, _("\n# Last %sSearch Pattern:\n~"), s); - /* off.dir is not stored, it's reset to forward */ - fprintf(fp, "%c%c%c%c%" PRId64 "%s%c", - spats[idx].magic ? 'M' : 'm', /* magic */ - spats[idx].no_scs ? 's' : 'S', /* smartcase */ - spats[idx].off.line ? 'L' : 'l', /* line offset */ - spats[idx].off.end ? 'E' : 'e', /* offset from end */ - (int64_t)spats[idx].off.off, /* offset */ - last_idx == idx ? "~" : "", /* last used pat */ - sc); - viminfo_writestring(fp, spats[idx].pat); - } + free_spat(&spats[1]); + memcpy(&(spats[1]), &pat, sizeof(spats[1])); + memset(&(spats[1].off), 0, sizeof(spats[1].off)); +} + +/// Set last used search pattern +/// +/// @param[in] is_substitute_pattern If true set substitute pattern as last +/// used. Otherwise sets search pattern. +void set_last_used_pattern(const bool is_substitute_pattern) +{ + last_idx = (is_substitute_pattern ? 1 : 0); +} + +/// Returns true if search pattern was the last used one +bool search_was_last_used(void) +{ + return last_idx == 0; } diff --git a/src/nvim/search.h b/src/nvim/search.h index 1bcde2c7ab..691782e41c 100644 --- a/src/nvim/search.h +++ b/src/nvim/search.h @@ -1,6 +1,9 @@ #ifndef NVIM_SEARCH_H #define NVIM_SEARCH_H +#include <stdbool.h> +#include <stdint.h> + /* Values for the find_pattern_in_path() function args 'type' and 'action': */ #define FIND_ANY 1 #define FIND_DEFINE 2 @@ -39,6 +42,27 @@ #define RE_BOTH 2 /* save pat in both patterns */ #define RE_LAST 2 /* use last used pattern if "pat" is NULL */ +/// Structure containing offset definition for the last search pattern +/// +/// @note Only offset for the last search pattern is used, not for the last +/// substitute pattern. +typedef struct soffset { + char dir; ///< Search direction: forward ('/') or backward ('?') + bool line; ///< True if search has line offset. + bool end; ///< True if search sets cursor at the end. + int64_t off; ///< Actual offset value. +} SearchOffset; + +/// Structure containing last search pattern and its attributes. +typedef struct spat { + char_u *pat; ///< The pattern (in allocated memory) or NULL. + bool magic; ///< Magicness of the pattern. + bool no_scs; ///< No smartcase for this pattern. + Timestamp timestamp; ///< Time of the last change. + SearchOffset off; ///< Pattern offset. + Dictionary *additional_data; ///< Additional data from ShaDa file. +} SearchPattern; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "search.h.generated.h" #endif diff --git a/src/nvim/shada.c b/src/nvim/shada.c new file mode 100644 index 0000000000..73aaa311b4 --- /dev/null +++ b/src/nvim/shada.c @@ -0,0 +1,3454 @@ +#include <stddef.h> +#include <stdbool.h> +#include <string.h> +#include <stdint.h> +#include <inttypes.h> +#include <errno.h> +#include <assert.h> + +#include <msgpack.h> + +#include "nvim/os/os.h" +#include "nvim/os/time.h" +#include "nvim/vim.h" +#include "nvim/ascii.h" +#include "nvim/shada.h" +#include "nvim/message.h" +#include "nvim/globals.h" +#include "nvim/macros.h" +#include "nvim/memory.h" +#include "nvim/mark.h" +#include "nvim/ops.h" +#include "nvim/garray.h" +#include "nvim/option.h" +#include "nvim/msgpack_rpc/helpers.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/globals.h" +#include "nvim/buffer.h" +#include "nvim/misc2.h" +#include "nvim/ex_getln.h" +#include "nvim/search.h" +#include "nvim/eval.h" +#include "nvim/eval_defs.h" +#include "nvim/version.h" +#include "nvim/path.h" + +#define buflist_nr2name(...) ((char *) buflist_nr2name(__VA_ARGS__)) +#define copy_option_part(src, dest, ...) \ + ((char *) copy_option_part((char_u **) src, (char_u *) dest, __VA_ARGS__)) +#define find_viminfo_parameter(...) \ + ((const char *) find_viminfo_parameter(__VA_ARGS__)) +#define emsg2(a, b) emsg2((char_u *) a, (char_u *) b) +#define emsg3(a, b, c) emsg3((char_u *) a, (char_u *) b, (char_u *) c) +#define emsgn(a, ...) emsgn((char_u *) a, __VA_ARGS__) +#define home_replace_save(a, b) \ + ((char *)home_replace_save(a, (char_u *)b)) + +/// Possible ShaDa entry types +/// +/// @warning Enum values are part of the API and must not be altered. +/// +/// All values that are not in enum are ignored. +typedef enum { + kSDItemUnknown = -1, ///< Unknown item. + kSDItemMissing = 0, ///< Missing value. Should never appear in a file. + kSDItemHeader = 1, ///< Header. Present for debugging purposes. + kSDItemSearchPattern = 2, ///< Last search pattern (*not* history item). + ///< Comes from user searches (e.g. when typing + ///< "/pat") or :substitute command calls. + kSDItemSubString = 3, ///< Last substitute replacement string. + kSDItemHistoryEntry = 4, ///< History item. + kSDItemRegister = 5, ///< Register. + kSDItemVariable = 6, ///< Global variable. + kSDItemGlobalMark = 7, ///< Global mark definition. + kSDItemJump = 8, ///< Item from jump list. + kSDItemBufferList = 9, ///< Buffer list. + kSDItemLocalMark = 10, ///< Buffer-local mark. +#define SHADA_LAST_ENTRY ((uint64_t) kSDItemLocalMark) +} ShadaEntryType; + +/// Structure defining a single ShaDa file entry +typedef struct { + ShadaEntryType type; + Timestamp timestamp; + union { + Dictionary header; + struct shada_filemark { + char name; + pos_T mark; + char *fname; + Dictionary *additional_data; + } filemark; + struct search_pattern { + bool magic; + bool smartcase; + bool has_line_offset; + bool place_cursor_at_end; + int64_t offset; + bool is_last_used; + bool is_substitute_pattern; + char *pat; + Dictionary *additional_data; + } search_pattern; + struct history_item { + uint8_t histtype; + char *string; + Array *additional_elements; + } history_item; + struct reg { + char name; + uint8_t type; + char **contents; + size_t contents_size; + size_t width; + Dictionary *additional_data; + } reg; + struct global_var { + char *name; + Object value; + Array *additional_elements; + } global_var; + struct { + uint64_t type; + char *contents; + size_t size; + } unknown_item; + struct sub_string { + char *sub; + Array *additional_elements; + } sub_string; + Array buffer_list; + } data; +} ShadaEntry; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "shada.c.generated.h" +#endif + +/// Msgpack callback for writing to FILE* +static int msgpack_fbuffer_write(void *data, const char *buf, size_t len) +{ + return (fwrite(buf, len, 1, (FILE *) data) == 1) ? 0 : -1; +} + +/// Check whether writing to shada file was disabled with -i NONE +/// +/// @return true if it was disabled, false otherwise. +static bool shada_disabled(void) + FUNC_ATTR_PURE +{ + return used_shada_file != NULL && STRCMP(used_shada_file, "NONE") == 0; +} + +/// Read ShaDa file +/// +/// @param[in] file File to read or NULL to use default name. +/// @param[in] flags Flags, see kShaDa enum values in shada.h. +/// +/// @return FAIL if reading failed for some reason and OK otherwise. +int shada_read_file(const char *const file, const int flags) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + FILE *fp; + + if (shada_disabled()) { + return FAIL; + } + + char *const fname = shada_filename(file); + fp = mch_fopen(fname, READBIN); + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Reading viminfo file \"%s\"%s%s%s"), + fname, + (flags & kShaDaWantInfo) ? _(" info") : "", + (flags & kShaDaWantMarks) ? _(" marks") : "", + (flags & kShaDaGetOldfiles) ? _(" oldfiles") : "", + fp == NULL ? _(" FAILED") : ""); + verbose_leave(); + } + + xfree(fname); + if (fp == NULL) { + return FAIL; + } + + shada_read(fp, flags); + + fclose(fp); + return OK; +} + +/// Read data from ShaDa file +/// +/// @param[in] fp File to read from. +/// @param[in] flags What to read. +static void shada_read(FILE *const fp, const int flags) + FUNC_ATTR_NONNULL_ALL +{ + ShadaEntry cur_entry; + buf_T *local_mark_prev_buf = NULL; + char *local_mark_prev_fname = NULL; + size_t local_mark_prev_fname_len = 0; + while (shada_read_next_item(fp, &cur_entry) == NOTDONE) { + switch (cur_entry.type) { + case kSDItemMissing: { + assert(false); + } + case kSDItemUnknown: { + break; + } + case kSDItemHeader: { + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemSearchPattern: { + if (!(flags & kShaDaWantInfo)) { + shada_free_shada_entry(&cur_entry); + break; + } + (cur_entry.data.search_pattern.is_substitute_pattern + ? &set_substitute_pattern + : &set_search_pattern)((SearchPattern) { + .magic = cur_entry.data.search_pattern.magic, + .no_scs = !cur_entry.data.search_pattern.smartcase, + .off = { + .line = cur_entry.data.search_pattern.has_line_offset, + .end = cur_entry.data.search_pattern.place_cursor_at_end, + .off = cur_entry.data.search_pattern.offset, + }, + .pat = (char_u *) cur_entry.data.search_pattern.pat, + .additional_data = cur_entry.data.search_pattern.additional_data, + .timestamp = cur_entry.timestamp, + }); + if (cur_entry.data.search_pattern.is_last_used) { + set_last_used_pattern( + cur_entry.data.search_pattern.is_substitute_pattern); + } + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemSubString: { + if (!(flags & kShaDaWantInfo)) { + shada_free_shada_entry(&cur_entry); + break; + } + sub_set_replacement((SubReplacementString) { + .sub = cur_entry.data.sub_string.sub, + .timestamp = cur_entry.timestamp, + .additional_elements = cur_entry.data.sub_string.additional_elements, + }); + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemHistoryEntry: { + if (!(flags & kShaDaWantInfo)) { + shada_free_shada_entry(&cur_entry); + break; + } + // FIXME + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemRegister: { + if (!(flags & kShaDaWantInfo)) { + shada_free_shada_entry(&cur_entry); + break; + } + if (cur_entry.data.reg.type != MCHAR + && cur_entry.data.reg.type != MLINE + && cur_entry.data.reg.type != MBLOCK) { + shada_free_shada_entry(&cur_entry); + break; + } + register_set(cur_entry.data.reg.name, (yankreg_T) { + .y_array = (char_u **) cur_entry.data.reg.contents, + .y_size = (linenr_T) cur_entry.data.reg.contents_size, + .y_type = cur_entry.data.reg.type, + .y_width = (colnr_T) cur_entry.data.reg.width, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.reg.additional_data, + }); + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemVariable: { + if (!(flags & kShaDaWantInfo) || find_viminfo_parameter('!') == NULL) { + shada_free_shada_entry(&cur_entry); + break; + } + typval_T vartv; + Error err; + if (!object_to_vim(cur_entry.data.global_var.value, &vartv, &err)) { + if (err.set) { + emsg3("Error while reading ShaDa file: " + "failed to read value for variable %s: %s", + cur_entry.data.global_var.name, err.msg); + } + break; + } + var_set_global(cur_entry.data.global_var.name, vartv); + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemGlobalMark: { + if (!(flags & kShaDaWantMarks) || get_viminfo_parameter('f') == 0) { + shada_free_shada_entry(&cur_entry); + break; + } + mark_set_global(cur_entry.data.filemark.name, (xfmark_T) { + .fname = (char_u *) cur_entry.data.filemark.fname, + .fmark = { + .mark = cur_entry.data.filemark.mark, + .fnum = 0, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.filemark.additional_data, + }, + }, !(flags & kShaDaForceit)); + break; + } + case kSDItemJump: { + // FIXME + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemBufferList: { + // FIXME + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemLocalMark: { + if (!(flags & kShaDaWantMarks)) { + shada_free_shada_entry(&cur_entry); + break; + } + buf_T *buf = NULL; + if (local_mark_prev_fname != NULL + && strncmp(local_mark_prev_fname, + cur_entry.data.filemark.fname, + local_mark_prev_fname_len) == 0) { + buf = local_mark_prev_buf; + } else { + FOR_ALL_BUFFERS(bp) { + if (bp->b_ffname != NULL && bp != local_mark_prev_buf) { + if (fnamecmp(cur_entry.data.filemark.fname, bp->b_ffname) == 0) { + buf = bp; + break; + } + } + } + xfree(local_mark_prev_fname); + local_mark_prev_buf = buf; + local_mark_prev_fname_len = strlen(cur_entry.data.filemark.fname); + local_mark_prev_fname = xmemdupz(cur_entry.data.filemark.fname, + local_mark_prev_fname_len); + } + if (buf == NULL) { + break; + } + mark_set_local( + cur_entry.data.filemark.name, buf, + (fmark_T) { + .mark = cur_entry.data.filemark.mark, + .fnum = 0, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.filemark.additional_data, + }, !(flags & kShaDaForceit)); + free(cur_entry.data.filemark.fname); + break; + } + } + } + xfree(local_mark_prev_fname); + FOR_ALL_BUFFERS(buf) { + fmarks_check_names(buf); + } +} + +/// Get the ShaDa file name to use +/// +/// If "file" is given and not empty, use it (has already been expanded by +/// cmdline functions). Otherwise use "-i file_name", value from 'viminfo' or +/// the default, and expand environment variables. +/// +/// @param[in] file Forced file name or NULL. +/// +/// @return An allocated string containing shada file name. +static char *shada_filename(const char *file) + FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (file == NULL || *file == NUL) { + if (used_shada_file != NULL) { + file = used_shada_file; + } else if ((file = find_viminfo_parameter('n')) == NULL || *file == NUL) { +#ifdef SHADA_FILE2 + // don't use $HOME when not defined (turned into "c:/"!). + if (os_getenv((char_u *)"HOME") == NULL) { + // don't use $VIM when not available. + expand_env((char_u *)"$VIM", NameBuff, MAXPATHL); + if (STRCMP("$VIM", NameBuff) != 0) { // $VIM was expanded + file = SHADA_FILE2; + } else { + file = SHADA_FILE; + } + } else { +#endif + file = SHADA_FILE; +#ifdef SHADA_FILE2 + } +#endif + // XXX It used to be one level lower, so that whatever is in + // `used_shada_file` was expanded. I intentionally moved it here + // because various expansions must have already be done by the shell. + // If shell is not performing them then they should be done in main.c + // where arguments are parsed, *not here*. + expand_env((char_u *)file, &(NameBuff[0]), MAXPATHL); + file = (const char *) &(NameBuff[0]); + } + } + return xstrdup(file); +} + +#define PACK_STATIC_STR(s) \ + do { \ + msgpack_pack_str(spacker, sizeof(s) - 1); \ + msgpack_pack_str_body(spacker, s, sizeof(s) - 1); \ + } while (0) + +/// Write single ShaDa entry +/// +/// @param[in] packer Packer used to write entry. +/// @param[in] entry Entry written. +/// @param[in] max_kbyte Maximum size of an item in KiB. Zero means no +/// restrictions. +static void shada_pack_entry(msgpack_packer *const packer, + const ShadaEntry entry, + const size_t max_kbyte) + FUNC_ATTR_NONNULL_ALL +{ + if (entry.type == kSDItemMissing) { + return; + } + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + msgpack_packer *spacker = msgpack_packer_new(&sbuf, &msgpack_sbuffer_write); + switch (entry.type) { + case kSDItemMissing: { + assert(false); + } + case kSDItemUnknown: { + msgpack_pack_uint64(packer, (uint64_t) entry.data.unknown_item.size); + packer->callback(packer->data, entry.data.unknown_item.contents, + (unsigned) entry.data.unknown_item.size); + break; + } + case kSDItemHistoryEntry: { + const size_t arr_size = 2 + ( + entry.data.history_item.additional_elements == NULL + ? 0 + : entry.data.history_item.additional_elements->size); + msgpack_pack_array(spacker, arr_size); + msgpack_pack_uint8(spacker, entry.data.history_item.histtype); + msgpack_rpc_from_string(cstr_as_string(entry.data.history_item.string), + spacker); + for (size_t i = 0; i < arr_size - 2; i++) { + msgpack_rpc_from_object( + entry.data.history_item.additional_elements->items[i], spacker); + } + break; + } + case kSDItemVariable: { + const size_t arr_size = 2 + ( + entry.data.global_var.additional_elements == NULL + ? 0 + : entry.data.global_var.additional_elements->size); + msgpack_pack_array(spacker, arr_size); + msgpack_rpc_from_string(cstr_as_string(entry.data.global_var.name), + spacker); + msgpack_rpc_from_object(entry.data.global_var.value, spacker); + for (size_t i = 0; i < arr_size - 2; i++) { + msgpack_rpc_from_object( + entry.data.global_var.additional_elements->items[i], spacker); + } + break; + } + case kSDItemSubString: { + const size_t arr_size = 1 + ( + entry.data.sub_string.additional_elements == NULL + ? 0 + : entry.data.sub_string.additional_elements->size); + msgpack_pack_array(spacker, arr_size); + msgpack_rpc_from_string(cstr_as_string(entry.data.sub_string.sub), + spacker); + for (size_t i = 0; i < arr_size - 1; i++) { + msgpack_rpc_from_object( + entry.data.sub_string.additional_elements->items[i], spacker); + } + break; + } + case kSDItemSearchPattern: { + const size_t map_size = (size_t) ( + 1 // Search pattern is always present + // Following items default to true: + + !entry.data.search_pattern.magic + + !entry.data.search_pattern.is_last_used + // Following items default to false: + + entry.data.search_pattern.smartcase + + entry.data.search_pattern.has_line_offset + + entry.data.search_pattern.place_cursor_at_end + + entry.data.search_pattern.is_substitute_pattern + // offset defaults to zero: + + (entry.data.search_pattern.offset != 0) + // finally, additional data: + + (entry.data.search_pattern.additional_data + ? entry.data.search_pattern.additional_data->size + : 0) + ); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR("pat"); + msgpack_rpc_from_string(cstr_as_string(entry.data.search_pattern.pat), + spacker); +#define PACK_BOOL(name, attr, nondef_value) \ + do { \ + if (entry.data.search_pattern.attr == nondef_value) { \ + PACK_STATIC_STR(name); \ + msgpack_pack_##nondef_value(spacker); \ + } \ + } while (0) + PACK_BOOL("magic", magic, false); + PACK_BOOL("islast", is_last_used, false); + PACK_BOOL("smartcase", smartcase, true); + PACK_BOOL("lineoff", has_line_offset, true); + PACK_BOOL("curatend", place_cursor_at_end, true); + PACK_BOOL("sub", is_substitute_pattern, true); + if (entry.data.search_pattern.offset) { + PACK_STATIC_STR("off"); + msgpack_pack_int64(spacker, entry.data.search_pattern.offset); + } +#undef PACK_BOOL + if (entry.data.search_pattern.additional_data != NULL) { + for (size_t i = 0; i < entry.data.search_pattern.additional_data->size; + i++) { + msgpack_rpc_from_string( + entry.data.search_pattern.additional_data->items[i].key, spacker); + msgpack_rpc_from_object( + entry.data.search_pattern.additional_data->items[i].value, + spacker); + } + } + break; + } + case kSDItemGlobalMark: + case kSDItemLocalMark: + case kSDItemJump: { + const size_t map_size = (size_t) ( + 1 // File name + // Line: defaults to 1 + + (entry.data.filemark.mark.lnum != 1) + // Column: defaults to zero: + + (entry.data.filemark.mark.col != 0) + // Mark name: defaults to '"' + + (entry.type != kSDItemJump + && entry.data.filemark.name != '"') + // Additional entries, if any: + + (entry.data.filemark.additional_data == NULL + ? 0 + : entry.data.filemark.additional_data->size) + ); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR("file"); + msgpack_rpc_from_string(cstr_as_string(entry.data.filemark.fname), + spacker); + if (entry.data.filemark.mark.lnum != 1) { + PACK_STATIC_STR("line"); + msgpack_pack_long(spacker, entry.data.filemark.mark.lnum); + } + if (entry.data.filemark.mark.col != 0) { + PACK_STATIC_STR("col"); + msgpack_pack_long(spacker, entry.data.filemark.mark.col); + } + if (entry.data.filemark.name != '"' && entry.type != kSDItemJump) { + PACK_STATIC_STR("name"); + msgpack_pack_uint8(spacker, (uint8_t) entry.data.filemark.name); + } + if (entry.data.filemark.additional_data != NULL) { + for (size_t i = 0; i < entry.data.filemark.additional_data->size; + i++) { + msgpack_rpc_from_string( + entry.data.filemark.additional_data->items[i].key, spacker); + msgpack_rpc_from_object( + entry.data.filemark.additional_data->items[i].value, spacker); + } + } + break; + } + case kSDItemRegister: { + const size_t map_size = (size_t) ( + 2 // Register contents and name + // Register type: defaults to MCHAR + + (entry.data.reg.type != MCHAR) + // Register width: defaults to zero + + (entry.data.reg.width != 0) + // Additional entries, if any: + + (entry.data.reg.additional_data == NULL + ? 0 + : entry.data.reg.additional_data->size) + ); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR("contents"); + msgpack_pack_array(spacker, entry.data.reg.contents_size); + for (size_t i = 0; i < entry.data.reg.contents_size; i++) { + msgpack_rpc_from_string(cstr_as_string(entry.data.reg.contents[i]), + spacker); + } + PACK_STATIC_STR("name"); + msgpack_pack_char(spacker, entry.data.reg.name); + if (entry.data.reg.type != MCHAR) { + PACK_STATIC_STR("type"); + msgpack_pack_uint8(spacker, entry.data.reg.type); + } + if (entry.data.reg.width != 0) { + PACK_STATIC_STR("width"); + msgpack_pack_uint64(spacker, (uint64_t) entry.data.reg.width); + } + if (entry.data.reg.additional_data != NULL) { + for (size_t i = 0; i < entry.data.reg.additional_data->size; + i++) { + msgpack_rpc_from_string(entry.data.reg.additional_data->items[i].key, + spacker); + msgpack_rpc_from_object( + entry.data.reg.additional_data->items[i].value, spacker); + } + } + break; + } + case kSDItemBufferList: { + msgpack_rpc_from_array(entry.data.buffer_list, spacker); + break; + } + case kSDItemHeader: { + msgpack_rpc_from_dictionary(entry.data.header, spacker); + break; + } + } + if (!max_kbyte || sbuf.size <= max_kbyte * 1024) { + if (entry.type == kSDItemUnknown) { + msgpack_pack_uint64(packer, (uint64_t) entry.data.unknown_item.type); + } else { + msgpack_pack_uint64(packer, (uint64_t) entry.type); + } + msgpack_pack_uint64(packer, (uint64_t) entry.timestamp); + if (sbuf.size > 0) { + msgpack_pack_uint64(packer, (uint64_t) sbuf.size); + packer->callback(packer->data, sbuf.data, (unsigned) sbuf.size); + } + } + msgpack_packer_free(spacker); + msgpack_sbuffer_destroy(&sbuf); +} + +/// Write ShaDa file +/// +/// @param[in] newfp File pointer to write to. Must not be NULL. +/// @param[in] oldfp Pointer to the previous ShaDa file. If it is not NULL +/// then contents of this file will be merged with current +/// NeoVim runtime. +static void shada_write(FILE *const newfp, FILE *const oldfp) + FUNC_ATTR_NONNULL_ARG(1) +{ + int max_kbyte_i = get_viminfo_parameter('s'); + if (max_kbyte_i < 0) { + max_kbyte_i = 10; + } + if (max_kbyte_i == 0) { + return; + } + const size_t max_kbyte = (size_t) max_kbyte_i; + + msgpack_packer *packer = msgpack_packer_new(newfp, &msgpack_fbuffer_write); + + // First write values that do not require merging + // 1. Header + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemHeader, + .timestamp = os_time(), + .data = { + .header = { + .size = 3, + .capacity = 3, + .items = ((KeyValuePair []) { + { STATIC_CSTR_AS_STRING("version"), + STRING_OBJ(cstr_as_string(longVersion)) }, + { STATIC_CSTR_AS_STRING("max_kbyte"), + INTEGER_OBJ((Integer) max_kbyte) }, + { STATIC_CSTR_AS_STRING("pid"), + INTEGER_OBJ((Integer) os_get_pid()) }, + }), + } + } + }, 0); + fflush(newfp); + + // 2. Buffer list + if (find_viminfo_parameter('%') != NULL) { + msgpack_pack_uint64(packer, (uint64_t) kSDItemBufferList); + msgpack_pack_uint64(packer, (uint64_t) os_time()); + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + msgpack_packer *spacker = msgpack_packer_new(&sbuf, &msgpack_sbuffer_write); + + size_t buf_count = 0; + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL) { + buf_count++; + } + } + msgpack_pack_array(packer, buf_count); + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname == NULL) { + continue; + } + msgpack_pack_map(packer, 3); + PACK_STATIC_STR("fname"); + msgpack_rpc_from_string(cstr_as_string((char *) buf->b_ffname), packer); + PACK_STATIC_STR("lnum"); + msgpack_pack_uint64(packer, (uint64_t) buf->b_last_cursor.mark.lnum); + PACK_STATIC_STR("col"); + msgpack_pack_uint64(packer, (uint64_t) buf->b_last_cursor.mark.col); + } + msgpack_pack_uint64(packer, (uint64_t) sbuf.size); + packer->callback(packer->data, sbuf.data, (unsigned) sbuf.size); + msgpack_packer_free(spacker); + msgpack_sbuffer_destroy(&sbuf); + } + + // 3. Jump list + const void *jump_iter = NULL; + do { + xfmark_T fm; + jump_iter = mark_jumplist_iter(jump_iter, curwin, &fm); + char *fname = (fm.fmark.fnum == 0 + ? (fm.fname == NULL + ? NULL + : xstrdup((char *) fm.fname)) + : buflist_nr2name(fm.fmark.fnum, true, false)); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemJump, + .timestamp = fm.fmark.timestamp, + .data = { + .filemark = { + .name = NUL, + .mark = fm.fmark.mark, + .fname = fname, + .additional_data = fm.fmark.additional_data, + } + } + }, max_kbyte); + xfree(fname); + } while (jump_iter != NULL); + + // FIXME No merging currently + + // 4. History + const void *hist_iters[HIST_COUNT] = {NULL, NULL, NULL, NULL, NULL}; + for (uint8_t i = 0; i < HIST_COUNT; i++) { + do { + histentry_T cur_hist; + hist_iters[i] = hist_iter(hist_iters[i], i, false, &cur_hist); + if (cur_hist.hisstr == NULL) { + break; + } + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemHistoryEntry, + .timestamp = cur_hist.timestamp, + .data = { + .history_item = { + .histtype = i, + .string = (char *) cur_hist.hisstr, + .additional_elements = cur_hist.additional_elements, + } + } + }, max_kbyte); + } while (hist_iters[i] != NULL); + } + + // 5. Search patterns + SearchPattern pat; + get_search_pattern(&pat); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemSearchPattern, + .timestamp = pat.timestamp, + .data = { + .search_pattern = { + .magic = pat.magic, + .smartcase = !pat.no_scs, + .has_line_offset = pat.off.line, + .place_cursor_at_end = pat.off.end, + .offset = pat.off.off, + .is_last_used = search_was_last_used(), + .is_substitute_pattern = false, + .pat = (char *) pat.pat, + .additional_data = pat.additional_data, + } + } + }, max_kbyte); + get_substitute_pattern(&pat); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemSearchPattern, + .timestamp = pat.timestamp, + .data = { + .search_pattern = { + .magic = pat.magic, + .smartcase = !pat.no_scs, + .has_line_offset = false, + .place_cursor_at_end = false, + .offset = 0, + .is_last_used = !search_was_last_used(), + .is_substitute_pattern = true, + .pat = (char *) pat.pat, + .additional_data = pat.additional_data, + } + } + }, max_kbyte); + + // 6. Substitute string + SubReplacementString sub; + sub_get_replacement(&sub); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemSubString, + .timestamp = sub.timestamp, + .data = { + .sub_string = { + .sub = (char *) sub.sub, + .additional_elements = sub.additional_elements, + } + } + }, max_kbyte); + + // 7. Global marks + if (get_viminfo_parameter('f') != 0) { + ShadaEntry *const global_marks = list_global_marks(); + for (ShadaEntry *mark = global_marks; mark->type != kSDItemMissing; mark++) { + shada_pack_entry(packer, *mark, max_kbyte); + } + xfree(global_marks); + } + + // 8. Buffer marks + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname == NULL) { + continue; + } + ShadaEntry *const buffer_marks = list_buffer_marks(buf); + for (ShadaEntry *mark = buffer_marks; mark->type != kSDItemMissing; + mark++) { + shada_pack_entry(packer, *mark, max_kbyte); + } + xfree(buffer_marks); + } + // FIXME: Copy previous marks, up to num_marked_files + // size_t num_marked_files = get_viminfo_parameter('\''); + + // 9. Registers + int max_num_lines_i = get_viminfo_parameter('<'); + if (max_num_lines_i < 0) { + max_num_lines_i = get_viminfo_parameter('"'); + } + if (max_num_lines_i != 0) { + const size_t max_num_lines = (max_num_lines_i < 0 + ? 0 + : (size_t) max_num_lines_i); + ShadaEntry *const registers = list_registers(max_num_lines); + for (ShadaEntry *reg = registers; reg->type != kSDItemMissing; reg++) { + shada_pack_entry(packer, *reg, max_kbyte); + } + xfree(registers); + } + + // 10. Variables + if (find_viminfo_parameter('!') != NULL) { + const void *var_iter = NULL; + const Timestamp cur_timestamp = os_time(); + do { + typval_T vartv; + const char *name; + var_iter = var_shada_iter(var_iter, &name, &vartv); + if (var_iter == NULL && vartv.v_type == VAR_UNKNOWN) { + break; + } + Object obj = vim_to_object(&vartv); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemVariable, + .timestamp = cur_timestamp, + .data = { + .global_var = { + .name = (char *) name, + .value = obj, + .additional_elements = NULL, + } + } + }, max_kbyte); + api_free_object(obj); + clear_tv(&vartv); + } while (var_iter != NULL); + } + + msgpack_packer_free(packer); +} + +#undef PACK_STATIC_STR + +/// Write ShaDa file to a given location +/// +/// @param[in] fname File to write to. If it is NULL or empty then default +/// location is used. +/// @param[in] nomerge If true then old file is ignored. +/// +/// @return OK if writing was successfull, FAIL otherwise. +int shada_write_file(const char *const file, const bool nomerge) +{ + char *const fname = shada_filename(file); + FILE *wfp = mch_fopen(fname, WRITEBIN); + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Writing viminfo file \"%s\""), fname); + verbose_leave(); + } + + xfree(fname); + if (wfp == NULL) { + return FAIL; + } + + shada_write(wfp, NULL); + + fclose(wfp); + return OK; +} + +/// Read marks information from ShaDa file +/// +/// @return OK in case of success, FAIL otherwise. +int shada_read_marks(void) +{ + return shada_read_file(NULL, kShaDaWantMarks); +} + +/// Read all information from ShaDa file +/// +/// @param[in] fname File to write to. If it is NULL or empty then default +/// +/// @return OK in case of success, FAIL otherwise. +int shada_read_everything(const char *const fname, const bool forceit) +{ + return shada_read_file(fname, + kShaDaWantInfo|kShaDaWantMarks|kShaDaGetOldfiles + |(forceit?kShaDaForceit:0)); +} + +static void shada_free_shada_entry(ShadaEntry *const entry) +{ + if (entry == NULL) { + return; + } + switch (entry->type) { + case kSDItemMissing: { + break; + } + case kSDItemUnknown: { + xfree(entry->data.unknown_item.contents); + break; + } + case kSDItemHeader: { + api_free_dictionary(entry->data.header); + break; + } + case kSDItemJump: + case kSDItemGlobalMark: + case kSDItemLocalMark: { + if (entry->data.filemark.additional_data != NULL) { + api_free_dictionary(*entry->data.filemark.additional_data); + xfree(entry->data.filemark.additional_data); + } + xfree(entry->data.filemark.fname); + break; + } + case kSDItemSearchPattern: { + if (entry->data.search_pattern.additional_data != NULL) { + api_free_dictionary(*entry->data.search_pattern.additional_data); + xfree(entry->data.search_pattern.additional_data); + } + xfree(entry->data.search_pattern.pat); + break; + } + case kSDItemRegister: { + if (entry->data.reg.additional_data != NULL) { + api_free_dictionary(*entry->data.reg.additional_data); + xfree(entry->data.reg.additional_data); + } + for (size_t i = 0; i < entry->data.reg.contents_size; i++) { + xfree(entry->data.reg.contents[i]); + } + xfree(entry->data.reg.contents); + break; + } + case kSDItemHistoryEntry: { + if (entry->data.history_item.additional_elements != NULL) { + api_free_array(*entry->data.history_item.additional_elements); + xfree(entry->data.history_item.additional_elements); + } + xfree(entry->data.history_item.string); + break; + } + case kSDItemVariable: { + if (entry->data.global_var.additional_elements != NULL) { + api_free_array(*entry->data.global_var.additional_elements); + xfree(entry->data.global_var.additional_elements); + } + xfree(entry->data.global_var.name); + api_free_object(entry->data.global_var.value); + break; + } + case kSDItemSubString: { + if (entry->data.sub_string.additional_elements != NULL) { + api_free_array(*entry->data.sub_string.additional_elements); + xfree(entry->data.sub_string.additional_elements); + } + xfree(entry->data.sub_string.sub); + break; + } + case kSDItemBufferList: { + api_free_array(entry->data.buffer_list); + break; + } + } +} + +/// Read next unsigned integer from file +/// +/// Errors out if the result is not an unsigned integer. +/// +/// Unlike msgpack own function this one works with `FILE *` and reads *exactly* +/// as much bytes as needed, making it possible to avoid both maintaining own +/// buffer and calling `fseek`. +/// +/// One byte from file stream is always consumed, even if it is not correct. +/// +/// @param[in] fp File to read from. +/// @param[out] result Location where result is saved. +/// +/// @return OK if read was successfull, FAIL if it was not. +static int msgpack_read_uint64(FILE *const fp, const int first_char, + uint64_t *const result) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + const long fpos = ftell(fp) - 1; + + if (first_char == EOF) { + if (ferror(fp)) { + emsg2("System error while reading ShaDa file: %s", + strerror(errno)); + } else if (feof(fp)) { + emsgn("Error while reading ShaDa file: " + "expected positive integer at position %" PRId64, + (int64_t) fpos); + } + return FAIL; + } + + if (~first_char & 0x80) { + // Positive fixnum + *result = (uint64_t) ((uint8_t) first_char); + } else { + size_t length = 0; + switch (first_char) { + case 0xCC: { // uint8 + length = 1; + break; + } + case 0xCD: { // uint16 + length = 2; + break; + } + case 0xCE: { // uint32 + length = 4; + break; + } + case 0xCF: { // uint64 + length = 8; + break; + } + default: { + emsgn("Error while reading ShaDa file: " + "expected positive integer at position %" PRId64, + (int64_t) fpos); + return FAIL; + } + } + uint8_t buf[8]; + size_t read_bytes = fread((char *) &(buf[0]), 1, length, fp); + if (ferror(fp)) { + emsg2("System error while reading ShaDa file: %s", + strerror(errno)); + return FAIL; + } else if (read_bytes != length) { + emsgn("Error while reading ShaDa file: " + "not enough bytes for positive integer at position %" PRId64, + (int64_t) fpos); + return FAIL; + } + // TODO(ZyX-I): Just cast if current platform is big-endian. + *result = 0; + for (size_t i = length; i; i--) { + *result |= ((uint64_t) buf[i - 1]) << ((length - i) * 8); + } + } + return OK; +} + +/// Iterate over shada file contents +/// +/// @param[in] fp Pointer to the opened ShaDa file. +/// @param[out] entry Address where next entry contents will be saved. +/// +/// @return NOTDONE if entry was read correctly, FAIL if there were errors and +/// OK at EOF. +static int shada_read_next_item(FILE *const fp, ShadaEntry *const entry) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + entry->type = kSDItemMissing; + if (feof(fp)) { + return OK; + } + + // First: manually unpack type, timestamp and length. + // This is needed to avoid both seeking and having to maintain a buffer. + uint64_t type_u64; + uint64_t timestamp_u64; + uint64_t length_u64; + + const long initial_fpos = ftell(fp); + const int first_char = fgetc(fp); + if (first_char == EOF && feof(fp)) { + return OK; + } + + if (msgpack_read_uint64(fp, first_char, &type_u64) != OK + || msgpack_read_uint64(fp, fgetc(fp), ×tamp_u64) != OK + || msgpack_read_uint64(fp, fgetc(fp), &length_u64) != OK) { + return FAIL; + } + if (type_u64 == 0) { + emsgn("Error while reading ShaDa file: " + "entry at position %" PRId64 "has invalid zero type", + (int64_t) initial_fpos); + return FAIL; + } + + const size_t length = (size_t) length_u64; + entry->timestamp = (Timestamp) timestamp_u64; + if (type_u64 > SHADA_LAST_ENTRY) { + entry->data.unknown_item.size = length; + char *contents = xmalloc(length); + entry->data.unknown_item.contents = contents; + entry->data.unknown_item.type = type_u64; + size_t read_bytes = fread(contents, 1, length, fp); + if (ferror(fp)) { + free(entry->data.unknown_item.contents); + emsg2("System error while reading ShaDa file: %s", + strerror(errno)); + return FAIL; + } else if (read_bytes != length) { + free(entry->data.unknown_item.contents); + emsgn("Error while reading ShaDa file: " + "last entry specified that it occupies %" PRId64 " bytes, " + "but file ended earlier", + (int64_t) length); + return FAIL; + } + entry->type = kSDItemUnknown; + return NOTDONE; + } + + msgpack_unpacked unpacked; + msgpack_unpacked_init(&unpacked); + msgpack_unpacker *unpacker = msgpack_unpacker_new(length); + if (unpacker == NULL || + !msgpack_unpacker_reserve_buffer(unpacker, length)) { + EMSG(e_outofmem); + goto shada_read_next_item_error; + } + msgpack_unpack_return result; + size_t read_bytes = 0; + + const long fpos = ftell(fp); + read_bytes = fread(msgpack_unpacker_buffer(unpacker), 1, length, fp); + if (ferror(fp)) { + emsg2("System error while reading ShaDa file: %s", + strerror(errno)); + goto shada_read_next_item_error; + } else if (read_bytes != length) { + emsgn("Error while reading ShaDa file: " + "last entry specified that it occupies %" PRId64 " bytes, " + "but file ended earlier", + (int64_t) fpos); + goto shada_read_next_item_error; + } + msgpack_unpacker_buffer_consumed(unpacker, read_bytes); + + result = msgpack_unpacker_next(unpacker, &unpacked); + if (result != MSGPACK_UNPACK_SUCCESS) { + if (result == MSGPACK_UNPACK_NOMEM_ERROR) { + EMSG(e_outofmem); + goto shada_read_next_item_error; + } + if (result == MSGPACK_UNPACK_PARSE_ERROR) { + EMSG("Failed to parse ShaDa file"); + goto shada_read_next_item_error; + } + } +#define CHECK_KEY(key, expected) \ + (key.via.str.size == sizeof(expected) - 1 \ + && STRNCMP(key.via.str.ptr, expected, sizeof(expected) - 1) == 0) +#define ID(s) s +#define BINDUP(b) xmemdupz(b.ptr, b.size) +#define TOINT(s) ((int) (s)) +#define TOLONG(s) ((long) (s)) +#define TOCHAR(s) ((char) (s)) +#define TOU8(s) ((uint8_t) (s)) +#define TOSIZE(s) ((size_t) (s)) +#define CHECKED_ENTRY(condition, error_desc, entry_name, obj, tgt, attr, \ + proc) \ + do { \ + if (!(condition)) { \ + emsgn("Error while reading ShaDa file: " \ + entry_name " entry at position %" PRId64 " " \ + error_desc, \ + (int64_t) fpos); \ + ga_clear(&ad_ga); \ + goto shada_read_next_item_error; \ + } \ + tgt = proc(obj.via.attr); \ + } while (0) +#define CHECK_KEY_IS_STR(entry_name) \ + do { \ + if (unpacked.data.via.map.ptr[i].key.type != MSGPACK_OBJECT_STR) { \ + emsgn("Error while reading ShaDa file: " \ + entry_name " entry at position %" PRId64 " " \ + "has key which is not a string", \ + (int64_t) fpos); \ + emsgn("It is %" PRId64 " instead", \ + unpacked.data.via.map.ptr[i].key.type ); \ + ga_clear(&ad_ga); \ + goto shada_read_next_item_error; \ + } \ + } while (0) +#define CHECKED_KEY(entry_name, name, error_desc, tgt, condition, attr, proc) \ + if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, name)) { \ + CHECKED_ENTRY( \ + condition, "has " name " key value " error_desc, \ + entry_name, unpacked.data.via.map.ptr[i].val, \ + tgt, attr, proc); \ + } +#define TYPED_KEY(entry_name, name, type_name, tgt, objtype, attr, proc) \ + CHECKED_KEY( \ + entry_name, name, " which is not " type_name, tgt, \ + unpacked.data.via.map.ptr[i].val.type == MSGPACK_OBJECT_##objtype, \ + attr, proc) +#define BOOLEAN_KEY(entry_name, name, tgt) \ + TYPED_KEY(entry_name, name, "a boolean", tgt, BOOLEAN, boolean, ID) +#define STRING_KEY(entry_name, name, tgt) \ + TYPED_KEY(entry_name, name, "a binary", tgt, BIN, bin, BINDUP) +#define INT_KEY(entry_name, name, tgt, proc) \ + CHECKED_KEY( \ + entry_name, name, " which is not an integer", tgt, \ + (unpacked.data.via.map.ptr[i].val.type \ + == MSGPACK_OBJECT_POSITIVE_INTEGER \ + || unpacked.data.via.map.ptr[i].val.type \ + == MSGPACK_OBJECT_NEGATIVE_INTEGER), \ + i64, proc) +#define INTEGER_KEY(entry_name, name, tgt) \ + INT_KEY(entry_name, name, tgt, TOINT) +#define LONG_KEY(entry_name, name, tgt) \ + INT_KEY(entry_name, name, tgt, TOLONG) +#define ADDITIONAL_KEY \ + { \ + ga_grow(&ad_ga, 1); \ + memcpy(((char *)ad_ga.ga_data) + ((size_t) ad_ga.ga_len \ + * sizeof(*unpacked.data.via.map.ptr)),\ + unpacked.data.via.map.ptr + i, \ + sizeof(*unpacked.data.via.map.ptr)); \ + ad_ga.ga_len++; \ + } + switch ((ShadaEntryType) type_u64) { + case kSDItemHeader: { + if (!msgpack_rpc_to_dictionary(&(unpacked.data), &(entry->data.header))) { + emsgn("Error while reading ShaDa file: " + "header entry at position %" PRId64 " is not a dictionary", + (int64_t) fpos); + goto shada_read_next_item_error; + } + break; + } + case kSDItemSearchPattern: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgn("Error while reading ShaDa file: " + "search pattern entry at position %" PRId64 " " + "is not a dictionary", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.search_pattern = (struct search_pattern) { + .magic = true, + .smartcase = false, + .has_line_offset = false, + .place_cursor_at_end = false, + .offset = 0, + .is_last_used = true, + .is_substitute_pattern = false, + .pat = NULL, + .additional_data = NULL, + }; + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("search pattern"); + BOOLEAN_KEY("search pattern", "magic", entry->data.search_pattern.magic) + else BOOLEAN_KEY("search pattern", "smartcase", + entry->data.search_pattern.smartcase) + else BOOLEAN_KEY("search pattern", "lineoff", + entry->data.search_pattern.has_line_offset) + else BOOLEAN_KEY("search pattern", "curatend", + entry->data.search_pattern.place_cursor_at_end) + else BOOLEAN_KEY("search pattern", "islast", + entry->data.search_pattern.is_last_used) + else BOOLEAN_KEY("search pattern", "sub", + entry->data.search_pattern.is_substitute_pattern) + else INTEGER_KEY("search pattern", "off", + entry->data.search_pattern.offset) + else STRING_KEY("search pattern", "pat", entry->data.search_pattern.pat) + else ADDITIONAL_KEY + } + if (entry->data.search_pattern.pat == NULL) { + emsgn("Error while reading ShaDa file: " + "search pattern entry at position %" PRId64 " " + "has no pattern", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + if (ad_ga.ga_len) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_MAP, + .via = { + .map = { + .size = (uint32_t) ad_ga.ga_len, + .ptr = ad_ga.ga_data, + } + } + }; + entry->data.search_pattern.additional_data = + xmalloc(sizeof(Dictionary)); + if (!msgpack_rpc_to_dictionary( + &obj, entry->data.search_pattern.additional_data)) { + emsgu("Error while reading ShaDa file: " + "search pattern entry at position %" PRIu64 " " + "cannot be converted to a Dictionary", + (uint64_t) initial_fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + } + ga_clear(&ad_ga); + break; + } + case kSDItemJump: + case kSDItemGlobalMark: + case kSDItemLocalMark: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgn("Error while reading ShaDa file: " + "mark entry at position %" PRId64 " " + "is not a dictionary", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.filemark = (struct shada_filemark) { + .name = '"', + .mark = (pos_T) {1, 0, 0}, + .fname = NULL, + .additional_data = NULL, + }; + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("mark"); + CHECKED_KEY( + "mark", "name", " which is not an unsigned integer", + entry->data.filemark.name, + (type_u64 != kSDItemJump + && unpacked.data.via.map.ptr[i].val.type + == MSGPACK_OBJECT_POSITIVE_INTEGER), + u64, TOCHAR) + else LONG_KEY("mark", "line", entry->data.filemark.mark.lnum) + else INTEGER_KEY("mark", "col", entry->data.filemark.mark.col) + else STRING_KEY("mark", "file", entry->data.filemark.fname) + else ADDITIONAL_KEY + } + if (entry->data.filemark.mark.lnum == 0) { + emsgn("Error while reading ShaDa file: " + "mark entry at position %" PRId64 " " + "is missing line number", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + if (ad_ga.ga_len) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_MAP, + .via = { + .map = { + .size = (uint32_t) ad_ga.ga_len, + .ptr = ad_ga.ga_data, + } + } + }; + entry->data.filemark.additional_data = xmalloc(sizeof(Dictionary)); + if (!msgpack_rpc_to_dictionary( + &obj, entry->data.filemark.additional_data)) { + emsgu("Error while reading ShaDa file: " + "mark entry at position %" PRIu64 " " + "cannot be converted to a Dictionary", + (uint64_t) initial_fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + } + ga_clear(&ad_ga); + break; + } + case kSDItemRegister: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "is not a dictionary", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.reg = (struct reg) { + .name = NUL, + .type = MCHAR, + .contents = NULL, + .contents_size = 0, + .width = 0, + .additional_data = NULL, + }; + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("register"); + TYPED_KEY("register", "type", "an unsigned integer", + entry->data.reg.type, POSITIVE_INTEGER, u64, TOU8) + else TYPED_KEY("register", "name", "an unsigned integer", + entry->data.reg.name, POSITIVE_INTEGER, u64, TOCHAR) + else TYPED_KEY("register", "width", "an unsigned integer", + entry->data.reg.width, POSITIVE_INTEGER, u64, TOSIZE) + else if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, "contents")) { + if (unpacked.data.via.map.ptr[i].val.type != MSGPACK_OBJECT_ARRAY) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "has contents key with non-array value", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + if (unpacked.data.via.map.ptr[i].val.via.array.size == 0) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "has contents key with empty array", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + const msgpack_object_array arr = + unpacked.data.via.map.ptr[i].val.via.array; + for (size_t i = 0; i < arr.size; i++) { + if (arr.ptr[i].type != MSGPACK_OBJECT_BIN) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "has contents array with non-string value", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + } + entry->data.reg.contents_size = arr.size; + entry->data.reg.contents = xmalloc(arr.size * sizeof(char *)); + for (size_t i = 0; i < arr.size; i++) { + entry->data.reg.contents[i] = xmemdupz(arr.ptr[i].via.bin.ptr, + arr.ptr[i].via.bin.size); + } + } else ADDITIONAL_KEY + } + if (entry->data.reg.contents == NULL) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "has missing contents array", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + if (ad_ga.ga_len) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_MAP, + .via = { + .map = { + .size = (uint32_t) ad_ga.ga_len, + .ptr = ad_ga.ga_data, + } + } + }; + entry->data.reg.additional_data = xmalloc(sizeof(Dictionary)); + if (!msgpack_rpc_to_dictionary( + &obj, entry->data.reg.additional_data)) { + emsgu("Error while reading ShaDa file: " + "register entry at position %" PRIu64 " " + "cannot be converted to a Dictionary", + (uint64_t) initial_fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + } + ga_clear(&ad_ga); + break; + } + case kSDItemHistoryEntry: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgn("Error while reading ShaDa file: " + "history entry at position %" PRId64 " " + "is not an array", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.history_item = (struct history_item) { + .histtype = 0, + .string = NULL, + .additional_elements = NULL + }; + if (unpacked.data.via.array.size < 2) { + emsgn("Error while reading ShaDa file: " + "history entry at position %" PRId64 " " + "does not have enough elements", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type + != MSGPACK_OBJECT_POSITIVE_INTEGER) { + emsgn("Error while reading ShaDa file: " + "history entry at position %" PRId64 " " + "has wrong history type type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[1].type + != MSGPACK_OBJECT_BIN) { + emsgn("Error while reading ShaDa file: " + "history entry at position %" PRId64 " " + "has wrong history string type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.history_item.histtype = + (uint8_t) unpacked.data.via.array.ptr[0].via.u64; + entry->data.history_item.string = + xmemdupz(unpacked.data.via.array.ptr[1].via.bin.ptr, + unpacked.data.via.array.ptr[1].via.bin.size); + if (unpacked.data.via.array.size > 2) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_ARRAY, + .via = { + .array = { + .size = unpacked.data.via.array.size - 2, + .ptr = unpacked.data.via.array.ptr + 2, + } + } + }; + entry->data.history_item.additional_elements = xmalloc(sizeof(Array)); + if (!msgpack_rpc_to_array( + &obj, entry->data.history_item.additional_elements)) { + emsgu("Error while reading ShaDa file: " + "history entry at position %" PRIu64 " " + "cannot be converted to an Array", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + } + break; + } + case kSDItemVariable: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgn("Error while reading ShaDa file: " + "variable entry at position %" PRId64 " " + "is not an array", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.global_var = (struct global_var) { + .name = NULL, + .value = { + .type = kObjectTypeNil, + }, + .additional_elements = NULL + }; + if (unpacked.data.via.array.size < 2) { + emsgn("Error while reading ShaDa file: " + "variable entry at position %" PRId64 " " + "does not have enough elements", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type != MSGPACK_OBJECT_BIN) { + emsgn("Error while reading ShaDa file: " + "variable entry at position %" PRId64 " " + "has wrong variable name type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[1].type == MSGPACK_OBJECT_NIL + || unpacked.data.via.array.ptr[1].type == MSGPACK_OBJECT_EXT) { + emsgn("Error while reading ShaDa file: " + "variable entry at position %" PRId64 " " + "has wrong variable value type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.global_var.name = + xmemdupz(unpacked.data.via.array.ptr[0].via.bin.ptr, + unpacked.data.via.array.ptr[0].via.bin.size); + if (!msgpack_rpc_to_object(&(unpacked.data.via.array.ptr[1]), + &(entry->data.global_var.value))) { + emsgu("Error while reading ShaDa file: " + "variable entry at position %" PRIu64 " " + "has value that cannot be converted to the object", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size > 2) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_ARRAY, + .via = { + .array = { + .size = unpacked.data.via.array.size - 2, + .ptr = unpacked.data.via.array.ptr + 2, + } + } + }; + entry->data.global_var.additional_elements = xmalloc(sizeof(Array)); + if (!msgpack_rpc_to_array( + &obj, entry->data.global_var.additional_elements)) { + emsgu("Error while reading ShaDa file: " + "variable entry at position %" PRIu64 " " + "cannot be converted to an Array", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + } + break; + } + case kSDItemSubString: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgn("Error while reading ShaDa file: " + "sub string entry at position %" PRId64 " " + "is not an array", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.sub_string = (struct sub_string) { + .sub = NULL, + .additional_elements = NULL + }; + if (unpacked.data.via.array.size < 1) { + emsgn("Error while reading ShaDa file: " + "sub string entry at position %" PRId64 " " + "does not have enough elements", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type != MSGPACK_OBJECT_BIN) { + emsgn("Error while reading ShaDa file: " + "sub string entry at position %" PRId64 " " + "has wrong sub string type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.sub_string.sub = + xmemdupz(unpacked.data.via.array.ptr[0].via.bin.ptr, + unpacked.data.via.array.ptr[0].via.bin.size); + if (unpacked.data.via.array.size > 1) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_ARRAY, + .via = { + .array = { + .size = unpacked.data.via.array.size - 1, + .ptr = unpacked.data.via.array.ptr + 1, + } + } + }; + entry->data.sub_string.additional_elements = xmalloc(sizeof(Array)); + if (!msgpack_rpc_to_array( + &obj, entry->data.sub_string.additional_elements)) { + emsgu("Error while reading ShaDa file: " + "sub string entry at position %" PRIu64 " " + "cannot be converted to an Array", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + } + break; + } + case kSDItemBufferList: { + if (!msgpack_rpc_to_array(&(unpacked.data), &(entry->data.buffer_list))) { + emsgn("Error while reading ShaDa file: " + "buffer list entry at position %" PRId64 " is not an array", + (int64_t) fpos); + goto shada_read_next_item_error; + } + break; + } + case kSDItemMissing: + case kSDItemUnknown: { + emsgu("Error while reading ShaDa file: " + "there is an item at position %" PRIu64 " " + "that must not be there: Missing and Unknown items are " + "for internal uses only", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + } + entry->type = (ShadaEntryType) type_u64; + goto shada_read_next_item_end; +#undef CHECK_KEY +#undef BOOLEAN_KEY +#undef ADDITIONAL_KEY +#undef ID +#undef BINDUP +#undef TOCHAR +#undef TOINT +#undef TOLONG +#undef TYPED_KEY +#undef INT_KEY +#undef INTEGER_KEY +#undef LONG_KEY +#undef TOU8 +#undef TOSIZE +shada_read_next_item_error: + msgpack_unpacked_destroy(&unpacked); + msgpack_unpacker_free(unpacker); + return FAIL; +shada_read_next_item_end: + msgpack_unpacked_destroy(&unpacked); + msgpack_unpacker_free(unpacker); + return NOTDONE; +} + +/// Return a list with all global marks set in current NeoVim instance +/// +/// List is not sorted. +/// +/// @warning Listed marks must be used before any buffer- or mark-editing +/// function is run. +/// +/// @return Array of ShadaEntry values, last one has type kSDItemMissing. +/// +/// @warning Resulting ShadaEntry values must not be freed. Returned +/// array must be freed with `xfree()`. +static ShadaEntry *list_global_marks(void) +{ + const void *iter = NULL; + const size_t nummarks = mark_global_amount(); + ShadaEntry *const ret = xmalloc(sizeof(ShadaEntry) * (nummarks + 1)); + ShadaEntry *cur = ret; + if (nummarks) { + do { + cur->type = kSDItemGlobalMark; + xfmark_T cur_fm; + iter = mark_global_iter(iter, &(cur->data.filemark.name), &cur_fm); + cur->data.filemark.mark = cur_fm.fmark.mark; + cur->data.filemark.additional_data = cur_fm.fmark.additional_data; + cur->timestamp = cur_fm.fmark.timestamp; + if (cur_fm.fmark.fnum == 0) { + cur->data.filemark.fname = (char *) (cur_fm.fname == NULL + ? NULL + : cur_fm.fname); + } else { + const buf_T *const buf = buflist_findnr(cur_fm.fmark.fnum); + if (buf == NULL) { + continue; + } else { + cur->data.filemark.fname = (char *) buf->b_ffname; + } + } + if (cur->data.filemark.fname != NULL) { + if (shada_removable(cur->data.filemark.fname)) { + continue; + } else { + cur++; + } + } + } while(iter != NULL); + } + cur->type = kSDItemMissing; + return ret; +} + +/// Return a list with all buffer marks set in some buffer +/// +/// List is not sorted. +/// +/// @warning Listed marks must be used before any buffer- or mark-editing +/// function is run. +/// +/// @param[in] buf Buffer for which marks are listed. +/// +/// @return Array of ShadaEntry values, last one has type kSDItemMissing. +/// +/// @warning Resulting ShadaEntry values must not be freed. Returned +/// array must be freed with `xfree()`. +static ShadaEntry *list_buffer_marks(const buf_T *const buf) +{ + const char *const fname = (char *) buf->b_ffname; + const void *iter = NULL; + const size_t nummarks = mark_buffer_amount(buf); + ShadaEntry *const ret = xmalloc(sizeof(ShadaEntry) * (nummarks + 1)); + ShadaEntry *cur = ret; + if (nummarks) { + do { + cur->type = kSDItemLocalMark; + fmark_T cur_fm; + iter = mark_buffer_iter(iter, buf, &(cur->data.filemark.name), &cur_fm); + cur->data.filemark.mark = cur_fm.mark; + cur->data.filemark.fname = (char *) fname; + cur->data.filemark.additional_data = cur_fm.additional_data; + cur->timestamp = cur_fm.timestamp; + if (cur->data.filemark.mark.lnum != 0) { + cur++; + } + } while(iter != NULL); + } + cur->type = kSDItemMissing; + return ret; +} + +/// Check whether "name" is on removable media (according to 'viminfo') +/// +/// @param[in] name Checked name. +/// +/// @return True if it is, false otherwise. +bool shada_removable(const char *name) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + char *p; + char part[51]; + int retval = FALSE; + size_t n; + + char *new_name = home_replace_save(NULL, name); + for (p = (char *) p_viminfo; *p; ) { + (void) copy_option_part(&p, part, 51, ", "); + if (part[0] == 'r') { + n = STRLEN(part + 1); + if (STRNICMP(part + 1, new_name, n) == 0) { + retval = TRUE; + break; + } + } + } + xfree(new_name); + return retval; +} + +/// Return a list with all registers and their contents +/// +/// List is not sorted. +/// +/// @warning Listed registers must be used before any register-editing function +/// is run. +/// +/// @param[in] max_num_lines Maximum number of lines in the register. If it is +/// zero then all registers are listed. +/// +/// @return Array of ShadaEntry values, last one has type kSDItemMissing. +/// +/// @warning Resulting ShadaEntry values must not be freed. Returned +/// array must be freed with `xfree()`. +static ShadaEntry *list_registers(const size_t max_num_lines) +{ + const void *iter = NULL; + const size_t numregs = op_register_amount(); + ShadaEntry *const ret = xmalloc(sizeof(ShadaEntry) * (numregs + 1)); + ShadaEntry *cur = ret; + if (numregs) { + do { + cur->type = kSDItemRegister; + yankreg_T cur_reg; + iter = op_register_iter(iter, &(cur->data.reg.name), &cur_reg); + if (max_num_lines && (size_t) cur_reg.y_size > max_num_lines) { + continue; + } + cur->data.reg.contents = (char **) cur_reg.y_array; + cur->data.reg.type = (uint8_t) cur_reg.y_type; + cur->data.reg.contents_size = (size_t) cur_reg.y_size; + if (cur_reg.y_type == MBLOCK) { + cur->data.reg.width = (size_t) cur_reg.y_width; + } else { + cur->data.reg.width = 0; + } + cur->data.reg.additional_data = cur_reg.additional_data; + cur->timestamp = cur_reg.timestamp; + cur++; + } while(iter != NULL); + } + cur->type = kSDItemMissing; + return ret; +} + + +#if 0 + +static int viminfo_errcnt; + +static int no_viminfo(void) +{ + /* "vim -i NONE" does not read or write a viminfo file */ + return use_viminfo != NULL && STRCMP(use_viminfo, "NONE") == 0; +} + +/* + * Report an error for reading a viminfo file. + * Count the number of errors. When there are more than 10, return TRUE. + */ +int viminfo_error(char *errnum, char *message, char_u *line) +{ + vim_snprintf((char *)IObuff, IOSIZE, _("%sviminfo: %s in line: "), + errnum, message); + STRNCAT(IObuff, line, IOSIZE - STRLEN(IObuff) - 1); + if (IObuff[STRLEN(IObuff) - 1] == '\n') + IObuff[STRLEN(IObuff) - 1] = NUL; + emsg(IObuff); + if (++viminfo_errcnt >= 10) { + EMSG(_("E136: viminfo: Too many errors, skipping rest of file")); + return TRUE; + } + return FALSE; +} + +/* + * read_viminfo() -- Read the viminfo file. Registers etc. which are already + * set are not over-written unless "flags" includes VIF_FORCEIT. -- webb + */ +int +read_viminfo ( + char_u *file, /* file name or NULL to use default name */ + int flags /* VIF_WANT_INFO et al. */ +) +{ + FILE *fp; + char_u *fname; + + if (no_viminfo()) + return FAIL; + + fname = viminfo_filename(file); /* get file name in allocated buffer */ + fp = mch_fopen((char *)fname, READBIN); + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Reading viminfo file \"%s\"%s%s%s"), + fname, + (flags & VIF_WANT_INFO) ? _(" info") : "", + (flags & VIF_WANT_MARKS) ? _(" marks") : "", + (flags & VIF_GET_OLDFILES) ? _(" oldfiles") : "", + fp == NULL ? _(" FAILED") : ""); + verbose_leave(); + } + + xfree(fname); + if (fp == NULL) + return FAIL; + + viminfo_errcnt = 0; + do_viminfo(fp, NULL, flags); + + fclose(fp); + return OK; +} + +/* + * Write the viminfo file. The old one is read in first so that effectively a + * merge of current info and old info is done. This allows multiple vims to + * run simultaneously, without losing any marks etc. + * If "forceit" is TRUE, then the old file is not read in, and only internal + * info is written to the file. + */ +void write_viminfo(char_u *file, int forceit) +{ + char_u *fname; + FILE *fp_in = NULL; /* input viminfo file, if any */ + FILE *fp_out = NULL; /* output viminfo file */ + char_u *tempname = NULL; /* name of temp viminfo file */ + char_u *wp; +#if defined(UNIX) + mode_t umask_save; +#endif + + if (no_viminfo()) + return; + + fname = viminfo_filename(file); /* may set to default if NULL */ + + fp_in = mch_fopen((char *)fname, READBIN); + if (fp_in == NULL) { + /* if it does exist, but we can't read it, don't try writing */ + if (os_file_exists(fname)) + goto end; +#if defined(UNIX) + /* + * For Unix we create the .viminfo non-accessible for others, + * because it may contain text from non-accessible documents. + */ + umask_save = umask(077); +#endif + fp_out = mch_fopen((char *)fname, WRITEBIN); +#if defined(UNIX) + (void)umask(umask_save); +#endif + } else { + /* + * There is an existing viminfo file. Create a temporary file to + * write the new viminfo into, in the same directory as the + * existing viminfo file, which will be renamed later. + */ +#ifdef UNIX + /* + * For Unix we check the owner of the file. It's not very nice to + * overwrite a user's viminfo file after a "su root", with a + * viminfo file that the user can't read. + */ + + FileInfo old_info; // FileInfo of existing viminfo file + if (os_fileinfo((char *)fname, &old_info) + && getuid() != ROOT_UID + && !(old_info.stat.st_uid == getuid() + ? (old_info.stat.st_mode & 0200) + : (old_info.stat.st_gid == getgid() + ? (old_info.stat.st_mode & 0020) + : (old_info.stat.st_mode & 0002)))) { + int tt = msg_didany; + + /* avoid a wait_return for this message, it's annoying */ + EMSG2(_("E137: Viminfo file is not writable: %s"), fname); + msg_didany = tt; + fclose(fp_in); + goto end; + } +#endif + + // Make tempname + tempname = (char_u *)modname((char *)fname, ".tmp", FALSE); + if (tempname != NULL) { + /* + * Check if tempfile already exists. Never overwrite an + * existing file! + */ + if (os_file_exists(tempname)) { + /* + * Try another name. Change one character, just before + * the extension. + */ + wp = tempname + STRLEN(tempname) - 5; + if (wp < path_tail(tempname)) /* empty file name? */ + wp = path_tail(tempname); + for (*wp = 'z'; os_file_exists(tempname); --*wp) { + /* + * They all exist? Must be something wrong! Don't + * write the viminfo file then. + */ + if (*wp == 'a') { + xfree(tempname); + tempname = NULL; + break; + } + } + } + } + + if (tempname != NULL) { + int fd; + + /* Use os_open() to be able to use O_NOFOLLOW and set file + * protection: + * Unix: same as original file, but strip s-bit. Reset umask to + * avoid it getting in the way. + * Others: r&w for user only. */ +# ifdef UNIX + umask_save = umask(0); + fd = os_open((char *)tempname, + O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW, + (int)((old_info.stat.st_mode & 0777) | 0600)); + (void)umask(umask_save); +# else + fd = os_open((char *)tempname, + O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW, 0600); +# endif + if (fd < 0) + fp_out = NULL; + else + fp_out = fdopen(fd, WRITEBIN); + + /* + * If we can't create in the same directory, try creating a + * "normal" temp file. + */ + if (fp_out == NULL) { + xfree(tempname); + if ((tempname = vim_tempname()) != NULL) + fp_out = mch_fopen((char *)tempname, WRITEBIN); + } + +#ifdef UNIX + /* + * Make sure the owner can read/write it. This only works for + * root. + */ + if (fp_out != NULL) { + os_fchown(fileno(fp_out), old_info.stat.st_uid, old_info.stat.st_gid); + } +#endif + } + } + + /* + * Check if the new viminfo file can be written to. + */ + if (fp_out == NULL) { + EMSG2(_("E138: Can't write viminfo file %s!"), + (fp_in == NULL || tempname == NULL) ? fname : tempname); + if (fp_in != NULL) + fclose(fp_in); + goto end; + } + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Writing viminfo file \"%s\""), fname); + verbose_leave(); + } + + viminfo_errcnt = 0; + do_viminfo(fp_in, fp_out, forceit ? 0 : (VIF_WANT_INFO | VIF_WANT_MARKS)); + + fclose(fp_out); /* errors are ignored !? */ + if (fp_in != NULL) { + fclose(fp_in); + + /* In case of an error keep the original viminfo file. Otherwise + * rename the newly written file. Give an error if that fails. */ + if (viminfo_errcnt == 0 && vim_rename(tempname, fname) == -1) { + viminfo_errcnt++; + EMSG2(_("E886: Can't rename viminfo file to %s!"), fname); + } + if (viminfo_errcnt > 0) { + os_remove((char *)tempname); + } + } + +end: + xfree(fname); + xfree(tempname); +} + +/* + * Get the viminfo file name to use. + * If "file" is given and not empty, use it (has already been expanded by + * cmdline functions). + * Otherwise use "-i file_name", value from 'viminfo' or the default, and + * expand environment variables. + * Returns an allocated string. + */ +static char_u *viminfo_filename(char_u *file) +{ + if (file == NULL || *file == NUL) { + if (use_viminfo != NULL) + file = use_viminfo; + else if ((file = find_viminfo_parameter('n')) == NULL || *file == NUL) { +#ifdef VIMINFO_FILE2 + // don't use $HOME when not defined (turned into "c:/"!). + if (!os_env_exists("HOME")) { + // don't use $VIM when not available. + expand_env((char_u *)"$VIM", NameBuff, MAXPATHL); + if (STRCMP("$VIM", NameBuff) != 0) /* $VIM was expanded */ + file = (char_u *)VIMINFO_FILE2; + else + file = (char_u *)VIMINFO_FILE; + } else +#endif + file = (char_u *)VIMINFO_FILE; + } + expand_env(file, NameBuff, MAXPATHL); + file = NameBuff; + } + return vim_strsave(file); +} + +/* + * do_viminfo() -- Should only be called from read_viminfo() & write_viminfo(). + */ +static void do_viminfo(FILE *fp_in, FILE *fp_out, int flags) +{ + int count = 0; + int eof = FALSE; + vir_T vir; + int merge = FALSE; + + vir.vir_line = xmalloc(LSIZE); + vir.vir_fd = fp_in; + vir.vir_conv.vc_type = CONV_NONE; + + if (fp_in != NULL) { + if (flags & VIF_WANT_INFO) { + eof = read_viminfo_up_to_marks(&vir, + flags & VIF_FORCEIT, fp_out != NULL); + merge = TRUE; + } else if (flags != 0) + /* Skip info, find start of marks */ + while (!(eof = viminfo_readline(&vir)) + && vir.vir_line[0] != '>') + ; + } + if (fp_out != NULL) { + /* Write the info: */ + fprintf(fp_out, _("# This viminfo file was generated by Nvim %s.\n"), + mediumVersion); + fputs(_("# You may edit it if you're careful!\n\n"), fp_out); + fputs(_("# Value of 'encoding' when this file was written\n"), fp_out); + fprintf(fp_out, "*encoding=%s\n\n", p_enc); + write_viminfo_search_pattern(fp_out); + write_viminfo_sub_string(fp_out); + write_viminfo_history(fp_out, merge); + write_viminfo_registers(fp_out); + write_viminfo_varlist(fp_out); + write_viminfo_filemarks(fp_out); + write_viminfo_bufferlist(fp_out); + count = write_viminfo_marks(fp_out); + } + if (fp_in != NULL + && (flags & (VIF_WANT_MARKS | VIF_GET_OLDFILES | VIF_FORCEIT))) + copy_viminfo_marks(&vir, fp_out, count, eof, flags); + + xfree(vir.vir_line); + if (vir.vir_conv.vc_type != CONV_NONE) + convert_setup(&vir.vir_conv, NULL, NULL); +} + +/* + * read_viminfo_up_to_marks() -- Only called from do_viminfo(). Reads in the + * first part of the viminfo file which contains everything but the marks that + * are local to a file. Returns TRUE when end-of-file is reached. -- webb + */ +static int read_viminfo_up_to_marks(vir_T *virp, int forceit, int writing) +{ + int eof; + + prepare_viminfo_history(forceit ? 9999 : 0, writing); + eof = viminfo_readline(virp); + while (!eof && virp->vir_line[0] != '>') { + switch (virp->vir_line[0]) { + /* Characters reserved for future expansion, ignored now */ + case '+': /* "+40 /path/dir file", for running vim without args */ + case '|': /* to be defined */ + case '^': /* to be defined */ + case '<': /* long line - ignored */ + /* A comment or empty line. */ + case NUL: + case '\r': + case '\n': + case '#': + eof = viminfo_readline(virp); + break; + case '*': /* "*encoding=value" */ + eof = viminfo_encoding(virp); + break; + case '!': /* global variable */ + eof = read_viminfo_varlist(virp, writing); + break; + case '%': /* entry for buffer list */ + eof = read_viminfo_bufferlist(virp, writing); + break; + case '"': + eof = read_viminfo_register(virp, forceit); + break; + case '/': /* Search string */ + case '&': /* Substitute search string */ + case '~': /* Last search string, followed by '/' or '&' */ + eof = read_viminfo_search_pattern(virp, forceit); + break; + case '$': + eof = read_viminfo_sub_string(virp, forceit); + break; + case ':': + case '?': + case '=': + case '@': + eof = read_viminfo_history(virp, writing); + break; + case '-': + case '\'': + eof = read_viminfo_filemark(virp, forceit); + break; + default: + if (viminfo_error("E575: ", _("Illegal starting char"), + virp->vir_line)) + eof = TRUE; + else + eof = viminfo_readline(virp); + break; + } + } + + /* Finish reading history items. */ + if (!writing) + finish_viminfo_history(); + + /* Change file names to buffer numbers for fmarks. */ + FOR_ALL_BUFFERS(buf) { + fmarks_check_names(buf); + } + + return eof; +} + +/* + * Compare the 'encoding' value in the viminfo file with the current value of + * 'encoding'. If different and the 'c' flag is in 'viminfo', setup for + * conversion of text with iconv() in viminfo_readstring(). + */ +static int viminfo_encoding(vir_T *virp) +{ + char_u *p; + int i; + + if (get_viminfo_parameter('c') != 0) { + p = vim_strchr(virp->vir_line, '='); + if (p != NULL) { + /* remove trailing newline */ + ++p; + for (i = 0; vim_isprintc(p[i]); ++i) + ; + p[i] = NUL; + + convert_setup(&virp->vir_conv, p, p_enc); + } + } + return viminfo_readline(virp); +} + +/* + * Read a line from the viminfo file. + * Returns TRUE for end-of-file; + */ +int viminfo_readline(vir_T *virp) +{ + return vim_fgets(virp->vir_line, LSIZE, virp->vir_fd); +} + +/* + * check string read from viminfo file + * remove '\n' at the end of the line + * - replace CTRL-V CTRL-V with CTRL-V + * - replace CTRL-V 'n' with '\n' + * + * Check for a long line as written by viminfo_writestring(). + * + * Return the string in allocated memory. + */ +char_u * +viminfo_readstring ( + vir_T *virp, + int off, /* offset for virp->vir_line */ + int convert /* convert the string */ +) + FUNC_ATTR_NONNULL_RET +{ + char_u *retval; + char_u *s, *d; + + if (virp->vir_line[off] == Ctrl_V && ascii_isdigit(virp->vir_line[off + 1])) { + ssize_t len = atol((char *)virp->vir_line + off + 1); + retval = xmalloc(len); + // TODO(philix): change type of vim_fgets() size argument to size_t + (void)vim_fgets(retval, (int)len, virp->vir_fd); + s = retval + 1; /* Skip the leading '<' */ + } else { + retval = vim_strsave(virp->vir_line + off); + s = retval; + } + + /* Change CTRL-V CTRL-V to CTRL-V and CTRL-V n to \n in-place. */ + d = retval; + while (*s != NUL && *s != '\n') { + if (s[0] == Ctrl_V && s[1] != NUL) { + if (s[1] == 'n') + *d++ = '\n'; + else + *d++ = Ctrl_V; + s += 2; + } else + *d++ = *s++; + } + *d = NUL; + + if (convert && virp->vir_conv.vc_type != CONV_NONE && *retval != NUL) { + d = string_convert(&virp->vir_conv, retval, NULL); + if (d != NULL) { + xfree(retval); + retval = d; + } + } + + return retval; +} + +/* + * write string to viminfo file + * - replace CTRL-V with CTRL-V CTRL-V + * - replace '\n' with CTRL-V 'n' + * - add a '\n' at the end + * + * For a long line: + * - write " CTRL-V <length> \n " in first line + * - write " < <string> \n " in second line + */ +void viminfo_writestring(FILE *fd, char_u *p) +{ + int c; + char_u *s; + int len = 0; + + for (s = p; *s != NUL; ++s) { + if (*s == Ctrl_V || *s == '\n') + ++len; + ++len; + } + + /* If the string will be too long, write its length and put it in the next + * line. Take into account that some room is needed for what comes before + * the string (e.g., variable name). Add something to the length for the + * '<', NL and trailing NUL. */ + if (len > LSIZE / 2) + fprintf(fd, "\026%d\n<", len + 3); + + while ((c = *p++) != NUL) { + if (c == Ctrl_V || c == '\n') { + putc(Ctrl_V, fd); + if (c == '\n') + c = 'n'; + } + putc(c, fd); + } + putc('\n', fd); +} + +/* + * Write all the named marks for all buffers. + * Return the number of buffers for which marks have been written. + */ +int write_viminfo_marks(FILE *fp_out) +{ + /* + * Set b_last_cursor for the all buffers that have a window. + */ + FOR_ALL_TAB_WINDOWS(tp, win) { + set_last_cursor(win); + } + + fputs(_("\n# History of marks within files (newest to oldest):\n"), fp_out); + int count = 0; + FOR_ALL_BUFFERS(buf) { + /* + * Only write something if buffer has been loaded and at least one + * mark is set. + */ + if (buf->b_marks_read) { + bool is_mark_set = true; + if (buf->b_last_cursor.lnum == 0) { + is_mark_set = false; + for (int i = 0; i < NMARKS; i++) { + if (buf->b_namedm[i].lnum != 0) { + is_mark_set = true; + break; + } + } + } + if (is_mark_set && buf->b_ffname != NULL + && buf->b_ffname[0] != NUL && !removable(buf->b_ffname)) { + home_replace(NULL, buf->b_ffname, IObuff, IOSIZE, TRUE); + fprintf(fp_out, "\n> "); + viminfo_writestring(fp_out, IObuff); + write_one_mark(fp_out, '"', &buf->b_last_cursor); + write_one_mark(fp_out, '^', &buf->b_last_insert); + write_one_mark(fp_out, '.', &buf->b_last_change); + /* changelist positions are stored oldest first */ + for (int i = 0; i < buf->b_changelistlen; ++i) { + write_one_mark(fp_out, '+', &buf->b_changelist[i]); + } + for (int i = 0; i < NMARKS; i++) { + write_one_mark(fp_out, 'a' + i, &buf->b_namedm[i]); + } + count++; + } + } + } + + return count; +} + +static void write_one_mark(FILE *fp_out, int c, pos_T *pos) +{ + if (pos->lnum != 0) + fprintf(fp_out, "\t%c\t%" PRId64 "\t%d\n", c, + (int64_t)pos->lnum, (int)pos->col); +} + +/* + * Handle marks in the viminfo file: + * fp_out != NULL: copy marks for buffers not in buffer list + * fp_out == NULL && (flags & VIF_WANT_MARKS): read marks for curbuf only + * fp_out == NULL && (flags & VIF_GET_OLDFILES | VIF_FORCEIT): fill v:oldfiles + */ +void copy_viminfo_marks(vir_T *virp, FILE *fp_out, int count, int eof, int flags) +{ + char_u *line = virp->vir_line; + buf_T *buf; + int num_marked_files; + int load_marks; + int copy_marks_out; + char_u *str; + int i; + char_u *p; + char_u *name_buf; + pos_T pos; + list_T *list = NULL; + + name_buf = xmalloc(LSIZE); + *name_buf = NUL; + + if (fp_out == NULL && (flags & (VIF_GET_OLDFILES | VIF_FORCEIT))) { + list = list_alloc(); + set_vim_var_list(VV_OLDFILES, list); + } + + num_marked_files = get_viminfo_parameter('\''); + while (!eof && (count < num_marked_files || fp_out == NULL)) { + if (line[0] != '>') { + if (line[0] != '\n' && line[0] != '\r' && line[0] != '#') { + if (viminfo_error("E576: ", _("Missing '>'"), line)) + break; /* too many errors, return now */ + } + eof = vim_fgets(line, LSIZE, virp->vir_fd); + continue; /* Skip this dud line */ + } + + /* + * Handle long line and translate escaped characters. + * Find file name, set str to start. + * Ignore leading and trailing white space. + */ + str = skipwhite(line + 1); + str = viminfo_readstring(virp, (int)(str - virp->vir_line), FALSE); + p = str + STRLEN(str); + while (p != str && (*p == NUL || ascii_isspace(*p))) + p--; + if (*p) + p++; + *p = NUL; + + if (list != NULL) + list_append_string(list, str, -1); + + /* + * If fp_out == NULL, load marks for current buffer. + * If fp_out != NULL, copy marks for buffers not in buflist. + */ + load_marks = copy_marks_out = FALSE; + if (fp_out == NULL) { + if ((flags & VIF_WANT_MARKS) && curbuf->b_ffname != NULL) { + if (*name_buf == NUL) /* only need to do this once */ + home_replace(NULL, curbuf->b_ffname, name_buf, LSIZE, TRUE); + if (fnamecmp(str, name_buf) == 0) + load_marks = TRUE; + } + } else { /* fp_out != NULL */ + /* This is slow if there are many buffers!! */ + buf = NULL; + FOR_ALL_BUFFERS(bp) { + if (bp->b_ffname != NULL) { + home_replace(NULL, bp->b_ffname, name_buf, LSIZE, TRUE); + if (fnamecmp(str, name_buf) == 0) { + buf = bp; + break; + } + } + } + + /* + * copy marks if the buffer has not been loaded + */ + if (buf == NULL || !buf->b_marks_read) { + copy_marks_out = TRUE; + fputs("\n> ", fp_out); + viminfo_writestring(fp_out, str); + count++; + } + } + free(str); + + pos.coladd = 0; + while (!(eof = viminfo_readline(virp)) && line[0] == TAB) { + if (load_marks) { + if (line[1] != NUL) { + int64_t lnum_64; + unsigned int u; + sscanf((char *)line + 2, "%" SCNd64 "%u", &lnum_64, &u); + // safely downcast to linenr_T (long); remove when linenr_T refactored + assert(lnum_64 <= LONG_MAX); + pos.lnum = (linenr_T)lnum_64; + assert(u <= INT_MAX); + pos.col = (colnr_T)u; + switch (line[1]) { + case '"': curbuf->b_last_cursor = pos; break; + case '^': curbuf->b_last_insert = pos; break; + case '.': curbuf->b_last_change = pos; break; + case '+': + /* changelist positions are stored oldest + * first */ + if (curbuf->b_changelistlen == JUMPLISTSIZE) + /* list is full, remove oldest entry */ + memmove(curbuf->b_changelist, + curbuf->b_changelist + 1, + sizeof(pos_T) * (JUMPLISTSIZE - 1)); + else + ++curbuf->b_changelistlen; + curbuf->b_changelist[ + curbuf->b_changelistlen - 1] = pos; + break; + default: if ((i = line[1] - 'a') >= 0 && i < NMARKS) + curbuf->b_namedm[i] = pos; + } + } + } else if (copy_marks_out) + fputs((char *)line, fp_out); + } + if (load_marks) { + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (wp->w_buffer == curbuf) + wp->w_changelistidx = curbuf->b_changelistlen; + } + break; + } + } + free(name_buf); +} + +int read_viminfo_filemark(vir_T *virp, int force) +{ + char_u *str; + xfmark_T *fm; + int i; + + /* We only get here if line[0] == '\'' or '-'. + * Illegal mark names are ignored (for future expansion). */ + str = virp->vir_line + 1; + if ( + *str <= 127 && + ((*virp->vir_line == '\'' && (ascii_isdigit(*str) || isupper(*str))) + || (*virp->vir_line == '-' && *str == '\''))) { + if (*str == '\'') { + /* If the jumplist isn't full insert fmark as oldest entry */ + if (curwin->w_jumplistlen == JUMPLISTSIZE) + fm = NULL; + else { + for (i = curwin->w_jumplistlen; i > 0; --i) + curwin->w_jumplist[i] = curwin->w_jumplist[i - 1]; + ++curwin->w_jumplistidx; + ++curwin->w_jumplistlen; + fm = &curwin->w_jumplist[0]; + fm->fmark.mark.lnum = 0; + fm->fname = NULL; + } + } else if (ascii_isdigit(*str)) + fm = &namedfm[*str - '0' + NMARKS]; + else { // is uppercase + assert(*str >= 'A' && *str <= 'Z'); + fm = &namedfm[*str - 'A']; + } + if (fm != NULL && (fm->fmark.mark.lnum == 0 || force)) { + str = skipwhite(str + 1); + fm->fmark.mark.lnum = getdigits_long(&str); + str = skipwhite(str); + fm->fmark.mark.col = getdigits_int(&str); + fm->fmark.mark.coladd = 0; + fm->fmark.fnum = 0; + str = skipwhite(str); + free(fm->fname); + fm->fname = viminfo_readstring(virp, (int)(str - virp->vir_line), + FALSE); + } + } + return vim_fgets(virp->vir_line, LSIZE, virp->vir_fd); +} + +void write_viminfo_filemarks(FILE *fp) +{ + int i; + char_u *name; + buf_T *buf; + xfmark_T *fm; + + if (get_viminfo_parameter('f') == 0) + return; + + fputs(_("\n# File marks:\n"), fp); + + /* + * Find a mark that is the same file and position as the cursor. + * That one, or else the last one is deleted. + * Move '0 to '1, '1 to '2, etc. until the matching one or '9 + * Set '0 mark to current cursor position. + */ + if (curbuf->b_ffname != NULL && !removable(curbuf->b_ffname)) { + name = buflist_nr2name(curbuf->b_fnum, TRUE, FALSE); + for (i = NMARKS; i < NMARKS + EXTRA_MARKS - 1; ++i) + if (namedfm[i].fmark.mark.lnum == curwin->w_cursor.lnum + && (namedfm[i].fname == NULL + ? namedfm[i].fmark.fnum == curbuf->b_fnum + : (name != NULL + && STRCMP(name, namedfm[i].fname) == 0))) + break; + free(name); + + free(namedfm[i].fname); + for (; i > NMARKS; --i) + namedfm[i] = namedfm[i - 1]; + namedfm[NMARKS].fmark.mark = curwin->w_cursor; + namedfm[NMARKS].fmark.fnum = curbuf->b_fnum; + namedfm[NMARKS].fname = NULL; + } + + /* Write the filemarks '0 - '9 and 'A - 'Z */ + for (i = 0; i < NMARKS + EXTRA_MARKS; i++) + write_one_filemark(fp, &namedfm[i], '\'', + i < NMARKS ? i + 'A' : i - NMARKS + '0'); + + /* Write the jumplist with -' */ + fputs(_("\n# Jumplist (newest first):\n"), fp); + setpcmark(); /* add current cursor position */ + cleanup_jumplist(); + for (fm = &curwin->w_jumplist[curwin->w_jumplistlen - 1]; + fm >= &curwin->w_jumplist[0]; --fm) { + if (fm->fmark.fnum == 0 + || ((buf = buflist_findnr(fm->fmark.fnum)) != NULL + && !removable(buf->b_ffname))) + write_one_filemark(fp, fm, '-', '\''); + } +} + +int read_viminfo_search_pattern(vir_T *virp, int force) +{ + char_u *lp; + int idx = -1; + int magic = FALSE; + int no_scs = FALSE; + int off_line = FALSE; + int off_end = 0; + long off = 0; + int setlast = FALSE; + static int hlsearch_on = FALSE; + char_u *val; + + /* + * Old line types: + * "/pat", "&pat": search/subst. pat + * "~/pat", "~&pat": last used search/subst. pat + * New line types: + * "~h", "~H": hlsearch highlighting off/on + * "~<magic><smartcase><line><end><off><last><which>pat" + * <magic>: 'm' off, 'M' on + * <smartcase>: 's' off, 'S' on + * <line>: 'L' line offset, 'l' char offset + * <end>: 'E' from end, 'e' from start + * <off>: decimal, offset + * <last>: '~' last used pattern + * <which>: '/' search pat, '&' subst. pat + */ + lp = virp->vir_line; + if (lp[0] == '~' && (lp[1] == 'm' || lp[1] == 'M')) { /* new line type */ + if (lp[1] == 'M') /* magic on */ + magic = TRUE; + if (lp[2] == 's') + no_scs = TRUE; + if (lp[3] == 'L') + off_line = TRUE; + if (lp[4] == 'E') + off_end = SEARCH_END; + lp += 5; + off = getdigits_long(&lp); + } + if (lp[0] == '~') { /* use this pattern for last-used pattern */ + setlast = TRUE; + lp++; + } + if (lp[0] == '/') + idx = RE_SEARCH; + else if (lp[0] == '&') + idx = RE_SUBST; + else if (lp[0] == 'h') /* ~h: 'hlsearch' highlighting off */ + hlsearch_on = FALSE; + else if (lp[0] == 'H') /* ~H: 'hlsearch' highlighting on */ + hlsearch_on = TRUE; + if (idx >= 0) { + if (force || spats[idx].pat == NULL) { + val = viminfo_readstring(virp, (int)(lp - virp->vir_line + 1), TRUE); + set_last_search_pat(val, idx, magic, setlast); + xfree(val); + spats[idx].no_scs = no_scs; + spats[idx].off.line = off_line; + spats[idx].off.end = off_end; + spats[idx].off.off = off; + if (setlast) { + SET_NO_HLSEARCH(!hlsearch_on); + } + } + } + return viminfo_readline(virp); +} + +void write_viminfo_search_pattern(FILE *fp) +{ + if (get_viminfo_parameter('/') != 0) { + fprintf(fp, "\n# hlsearch on (H) or off (h):\n~%c", + (no_hlsearch || find_viminfo_parameter('h') != NULL) ? 'h' : 'H'); + wvsp_one(fp, RE_SEARCH, "", '/'); + wvsp_one(fp, RE_SUBST, _("Substitute "), '&'); + } +} + +static void +wvsp_one ( + FILE *fp, /* file to write to */ + int idx, /* spats[] index */ + char *s, /* search pat */ + int sc /* dir char */ +) +{ + if (spats[idx].pat != NULL) { + fprintf(fp, _("\n# Last %sSearch Pattern:\n~"), s); + /* off.dir is not stored, it's reset to forward */ + fprintf(fp, "%c%c%c%c%" PRId64 "%s%c", + spats[idx].magic ? 'M' : 'm', /* magic */ + spats[idx].no_scs ? 's' : 'S', /* smartcase */ + spats[idx].off.line ? 'L' : 'l', /* line offset */ + spats[idx].off.end ? 'E' : 'e', /* offset from end */ + (int64_t)spats[idx].off.off, /* offset */ + last_idx == idx ? "~" : "", /* last used pat */ + sc); + viminfo_writestring(fp, spats[idx].pat); + } +} + +/* + * Prepare for reading the history from the viminfo file. + * This allocates history arrays to store the read history lines. + */ +void prepare_viminfo_history(int asklen, int writing) +{ + int i; + int num; + + init_history(); + viminfo_add_at_front = (asklen != 0 && !writing); + if (asklen > hislen) + asklen = hislen; + + for (int type = 0; type < HIST_COUNT; ++type) { + /* Count the number of empty spaces in the history list. Entries read + * from viminfo previously are also considered empty. If there are + * more spaces available than we request, then fill them up. */ + for (i = 0, num = 0; i < hislen; i++) + if (history[type][i].hisstr == NULL || history[type][i].viminfo) + num++; + int len = asklen; + if (num > len) + len = num; + if (len <= 0) + viminfo_history[type] = NULL; + else + viminfo_history[type] = xmalloc(len * sizeof(char_u *)); + if (viminfo_history[type] == NULL) + len = 0; + viminfo_hislen[type] = len; + viminfo_hisidx[type] = 0; + } +} + +/* + * Accept a line from the viminfo, store it in the history array when it's + * new. + */ +int read_viminfo_history(vir_T *virp, int writing) +{ + int type; + char_u *val; + + type = hist_char2type(virp->vir_line[0]); + if (viminfo_hisidx[type] < viminfo_hislen[type]) { + val = viminfo_readstring(virp, 1, TRUE); + if (val != NULL && *val != NUL) { + int sep = (*val == ' ' ? NUL : *val); + + if (!in_history(type, val + (type == HIST_SEARCH), + viminfo_add_at_front, sep, writing)) { + /* Need to re-allocate to append the separator byte. */ + size_t len = STRLEN(val); + char_u *p = xmalloc(len + 2); + if (type == HIST_SEARCH) { + /* Search entry: Move the separator from the first + * column to after the NUL. */ + memmove(p, val + 1, len); + p[len] = sep; + } else { + /* Not a search entry: No separator in the viminfo + * file, add a NUL separator. */ + memmove(p, val, len + 1); + p[len + 1] = NUL; + } + viminfo_history[type][viminfo_hisidx[type]++] = p; + } + } + xfree(val); + } + return viminfo_readline(virp); +} + +/* + * Finish reading history lines from viminfo. Not used when writing viminfo. + */ +void finish_viminfo_history(void) +{ + int idx; + int i; + int type; + + for (type = 0; type < HIST_COUNT; ++type) { + if (history[type] == NULL) + continue; + idx = hisidx[type] + viminfo_hisidx[type]; + if (idx >= hislen) + idx -= hislen; + else if (idx < 0) + idx = hislen - 1; + if (viminfo_add_at_front) + hisidx[type] = idx; + else { + if (hisidx[type] == -1) + hisidx[type] = hislen - 1; + do { + if (history[type][idx].hisstr != NULL + || history[type][idx].viminfo) + break; + if (++idx == hislen) + idx = 0; + } while (idx != hisidx[type]); + if (idx != hisidx[type] && --idx < 0) + idx = hislen - 1; + } + for (i = 0; i < viminfo_hisidx[type]; i++) { + xfree(history[type][idx].hisstr); + history[type][idx].hisstr = viminfo_history[type][i]; + history[type][idx].viminfo = TRUE; + if (--idx < 0) + idx = hislen - 1; + } + idx += 1; + idx %= hislen; + for (i = 0; i < viminfo_hisidx[type]; i++) { + history[type][idx++].hisnum = ++hisnum[type]; + idx %= hislen; + } + xfree(viminfo_history[type]); + viminfo_history[type] = NULL; + viminfo_hisidx[type] = 0; + } +} + +/* + * Write history to viminfo file in "fp". + * When "merge" is TRUE merge history lines with a previously read viminfo + * file, data is in viminfo_history[]. + * When "merge" is FALSE just write all history lines. Used for ":wviminfo!". + */ +void write_viminfo_history(FILE *fp, int merge) +{ + int i; + int type; + int num_saved; + char_u *p; + int c; + int round; + + init_history(); + if (hislen == 0) + return; + for (type = 0; type < HIST_COUNT; ++type) { + num_saved = get_viminfo_parameter(hist_type2char(type, FALSE)); + if (num_saved == 0) + continue; + if (num_saved < 0) /* Use default */ + num_saved = hislen; + fprintf(fp, _("\n# %s History (newest to oldest):\n"), + type == HIST_CMD ? _("Command Line") : + type == HIST_SEARCH ? _("Search String") : + type == HIST_EXPR ? _("Expression") : + _("Input Line")); + if (num_saved > hislen) + num_saved = hislen; + + /* + * Merge typed and viminfo history: + * round 1: history of typed commands. + * round 2: history from recently read viminfo. + */ + for (round = 1; round <= 2; ++round) { + if (round == 1) + /* start at newest entry, somewhere in the list */ + i = hisidx[type]; + else if (viminfo_hisidx[type] > 0) + /* start at newest entry, first in the list */ + i = 0; + else + /* empty list */ + i = -1; + if (i >= 0) + while (num_saved > 0 + && !(round == 2 && i >= viminfo_hisidx[type])) { + p = round == 1 ? history[type][i].hisstr + : viminfo_history[type] == NULL ? NULL + : viminfo_history[type][i]; + if (p != NULL && (round == 2 + || !merge + || !history[type][i].viminfo)) { + --num_saved; + fputc(hist_type2char(type, TRUE), fp); + /* For the search history: put the separator in the + * second column; use a space if there isn't one. */ + if (type == HIST_SEARCH) { + c = p[STRLEN(p) + 1]; + putc(c == NUL ? ' ' : c, fp); + } + viminfo_writestring(fp, p); + } + if (round == 1) { + /* Decrement index, loop around and stop when back at + * the start. */ + if (--i < 0) + i = hislen - 1; + if (i == hisidx[type]) + break; + } else { + /* Increment index. Stop at the end in the while. */ + ++i; + } + } + } + for (i = 0; i < viminfo_hisidx[type]; ++i) + if (viminfo_history[type] != NULL) + xfree(viminfo_history[type][i]); + xfree(viminfo_history[type]); + viminfo_history[type] = NULL; + viminfo_hisidx[type] = 0; + } +} + +int read_viminfo_register(vir_T *virp, int force) +{ + int eof; + int do_it = TRUE; + int size; + int limit; + int set_prev = FALSE; + char_u *str; + char_u **array = NULL; + + /* We only get here (hopefully) if line[0] == '"' */ + str = virp->vir_line + 1; + + /* If the line starts with "" this is the y_previous register. */ + if (*str == '"') { + set_prev = TRUE; + str++; + } + + if (!ASCII_ISALNUM(*str) && *str != '-') { + if (viminfo_error("E577: ", _("Illegal register name"), virp->vir_line)) + return TRUE; /* too many errors, pretend end-of-file */ + do_it = FALSE; + } + yankreg_T *reg = get_yank_register(*str++, YREG_PUT); + if (!force && reg->y_array != NULL) + do_it = FALSE; + + if (*str == '@') { + /* "x@: register x used for @@ */ + if (force || execreg_lastc == NUL) + execreg_lastc = str[-1]; + } + + size = 0; + limit = 100; /* Optimized for registers containing <= 100 lines */ + if (do_it) { + if (set_prev) { + y_previous = reg; + } + + free_register(reg); + array = xmalloc(limit * sizeof(char_u *)); + + str = skipwhite(skiptowhite(str)); + if (STRNCMP(str, "CHAR", 4) == 0) { + reg->y_type = MCHAR; + } else if (STRNCMP(str, "BLOCK", 5) == 0) { + reg->y_type = MBLOCK; + } else { + reg->y_type = MLINE; + } + /* get the block width; if it's missing we get a zero, which is OK */ + str = skipwhite(skiptowhite(str)); + reg->y_width = getdigits_int(&str); + } + + while (!(eof = viminfo_readline(virp)) + && (virp->vir_line[0] == TAB || virp->vir_line[0] == '<')) { + if (do_it) { + if (size >= limit) { + limit *= 2; + array = xrealloc(array, limit * sizeof(char_u *)); + } + array[size++] = viminfo_readstring(virp, 1, TRUE); + } + } + + if (do_it) { + if (size == 0) { + xfree(array); + } else if (size < limit) { + reg->y_array = xrealloc(array, size * sizeof(char_u *)); + } else { + reg->y_array = array; + } + reg->y_size = size; + } + return eof; +} + +void write_viminfo_registers(FILE *fp) +{ + int i, j; + char_u *type; + char_u c; + int num_lines; + int max_num_lines; + int max_kbyte; + long len; + + fputs(_("\n# Registers:\n"), fp); + + /* Get '<' value, use old '"' value if '<' is not found. */ + max_num_lines = get_viminfo_parameter('<'); + if (max_num_lines < 0) + max_num_lines = get_viminfo_parameter('"'); + if (max_num_lines == 0) + return; + max_kbyte = get_viminfo_parameter('s'); + if (max_kbyte == 0) + return; + + // don't include clipboard registers '*'/'+' + for (i = 0; i < NUM_SAVED_REGISTERS; i++) { + if (y_regs[i].y_array == NULL) + continue; + + /* Skip empty registers. */ + num_lines = y_regs[i].y_size; + if (num_lines == 0 + || (num_lines == 1 && y_regs[i].y_type == MCHAR + && *y_regs[i].y_array[0] == NUL)) + continue; + + if (max_kbyte > 0) { + /* Skip register if there is more text than the maximum size. */ + len = 0; + for (j = 0; j < num_lines; j++) + len += (long)STRLEN(y_regs[i].y_array[j]) + 1L; + if (len > (long)max_kbyte * 1024L) + continue; + } + + switch (y_regs[i].y_type) { + case MLINE: + type = (char_u *)"LINE"; + break; + case MCHAR: + type = (char_u *)"CHAR"; + break; + case MBLOCK: + type = (char_u *)"BLOCK"; + break; + default: + sprintf((char *)IObuff, _("E574: Unknown register type %d"), + y_regs[i].y_type); + emsg(IObuff); + type = (char_u *)"LINE"; + break; + } + if (y_previous == &y_regs[i]) + fprintf(fp, "\""); + c = get_register_name(i); + fprintf(fp, "\"%c", c); + if (c == execreg_lastc) + fprintf(fp, "@"); + fprintf(fp, "\t%s\t%d\n", type, + (int)y_regs[i].y_width + ); + + /* If max_num_lines < 0, then we save ALL the lines in the register */ + if (max_num_lines > 0 && num_lines > max_num_lines) + num_lines = max_num_lines; + for (j = 0; j < num_lines; j++) { + putc('\t', fp); + viminfo_writestring(fp, y_regs[i].y_array[j]); + } + } +} + +/* + * Restore global vars that start with a capital from the viminfo file + */ +int read_viminfo_varlist(vir_T *virp, int writing) +{ + char_u *tab; + int type = VAR_NUMBER; + typval_T tv; + + if (!writing && (find_viminfo_parameter('!') != NULL)) { + tab = vim_strchr(virp->vir_line + 1, '\t'); + if (tab != NULL) { + *tab++ = NUL; /* isolate the variable name */ + switch (*tab) { + case 'S': type = VAR_STRING; break; + case 'F': type = VAR_FLOAT; break; + case 'D': type = VAR_DICT; break; + case 'L': type = VAR_LIST; break; + } + + tab = vim_strchr(tab, '\t'); + if (tab != NULL) { + tv.v_type = type; + if (type == VAR_STRING || type == VAR_DICT || type == VAR_LIST) + tv.vval.v_string = viminfo_readstring(virp, + (int)(tab - virp->vir_line + 1), TRUE); + else if (type == VAR_FLOAT) + (void)string2float(tab + 1, &tv.vval.v_float); + else + tv.vval.v_number = atol((char *)tab + 1); + if (type == VAR_DICT || type == VAR_LIST) { + typval_T *etv = eval_expr(tv.vval.v_string, NULL); + + if (etv == NULL) + /* Failed to parse back the dict or list, use it as a + * string. */ + tv.v_type = VAR_STRING; + else { + free(tv.vval.v_string); + tv = *etv; + free(etv); + } + } + + set_var(virp->vir_line + 1, &tv, FALSE); + + if (tv.v_type == VAR_STRING) + free(tv.vval.v_string); + else if (tv.v_type == VAR_DICT || tv.v_type == VAR_LIST) + clear_tv(&tv); + } + } + } + + return viminfo_readline(virp); +} + +/* + * Write global vars that start with a capital to the viminfo file + */ +void write_viminfo_varlist(FILE *fp) +{ + hashitem_T *hi; + dictitem_T *this_var; + int todo; + char *s; + char_u *p; + char_u *tofree; + char_u numbuf[NUMBUFLEN]; + + if (find_viminfo_parameter('!') == NULL) + return; + + fputs(_("\n# global variables:\n"), fp); + + todo = (int)globvarht.ht_used; + for (hi = globvarht.ht_array; todo > 0; ++hi) { + if (!HASHITEM_EMPTY(hi)) { + --todo; + this_var = HI2DI(hi); + if (var_flavour(this_var->di_key) == VAR_FLAVOUR_VIMINFO) { + switch (this_var->di_tv.v_type) { + case VAR_STRING: s = "STR"; break; + case VAR_NUMBER: s = "NUM"; break; + case VAR_FLOAT: s = "FLO"; break; + case VAR_DICT: s = "DIC"; break; + case VAR_LIST: s = "LIS"; break; + default: continue; + } + fprintf(fp, "!%s\t%s\t", this_var->di_key, s); + p = echo_string(&this_var->di_tv, &tofree, numbuf, 0); + if (p != NULL) + viminfo_writestring(fp, p); + free(tofree); + } + } + } +} + +int read_viminfo_bufferlist(vir_T *virp, int writing) +{ + char_u *tab; + linenr_T lnum; + colnr_T col; + buf_T *buf; + char_u *sfname; + char_u *xline; + + /* Handle long line and escaped characters. */ + xline = viminfo_readstring(virp, 1, FALSE); + + /* don't read in if there are files on the command-line or if writing: */ + if (xline != NULL && !writing && ARGCOUNT == 0 + && find_viminfo_parameter('%') != NULL) { + /* Format is: <fname> Tab <lnum> Tab <col>. + * Watch out for a Tab in the file name, work from the end. */ + lnum = 0; + col = 0; + tab = vim_strrchr(xline, '\t'); + if (tab != NULL) { + *tab++ = '\0'; + col = (colnr_T)atoi((char *)tab); + tab = vim_strrchr(xline, '\t'); + if (tab != NULL) { + *tab++ = '\0'; + lnum = atol((char *)tab); + } + } + + /* Expand "~/" in the file name at "line + 1" to a full path. + * Then try shortening it by comparing with the current directory */ + expand_env(xline, NameBuff, MAXPATHL); + sfname = path_shorten_fname_if_possible(NameBuff); + + buf = buflist_new(NameBuff, sfname, (linenr_T)0, BLN_LISTED); + if (buf != NULL) { /* just in case... */ + buf->b_last_cursor.lnum = lnum; + buf->b_last_cursor.col = col; + buflist_setfpos(buf, curwin, lnum, col, FALSE); + } + } + xfree(xline); + + return viminfo_readline(virp); +} + +void write_viminfo_bufferlist(FILE *fp) +{ + char_u *line; + int max_buffers; + + if (find_viminfo_parameter('%') == NULL) + return; + + /* Without a number -1 is returned: do all buffers. */ + max_buffers = get_viminfo_parameter('%'); + + /* Allocate room for the file name, lnum and col. */ +#define LINE_BUF_LEN (MAXPATHL + 40) + line = xmalloc(LINE_BUF_LEN); + + FOR_ALL_TAB_WINDOWS(tp, win) { + set_last_cursor(win); + } + + fputs(_("\n# Buffer list:\n"), fp); + FOR_ALL_BUFFERS(buf) { + if (buf->b_fname == NULL + || !buf->b_p_bl + || bt_quickfix(buf) + || removable(buf->b_ffname)) + continue; + + if (max_buffers-- == 0) + break; + putc('%', fp); + home_replace(NULL, buf->b_ffname, line, MAXPATHL, TRUE); + vim_snprintf_add((char *)line, LINE_BUF_LEN, "\t%" PRId64 "\t%d", + (int64_t)buf->b_last_cursor.lnum, + buf->b_last_cursor.col); + viminfo_writestring(fp, line); + } + xfree(line); +} + +int read_viminfo_sub_string(vir_T *virp, int force) +{ + if (force) + xfree(old_sub); + if (force || old_sub == NULL) + old_sub = viminfo_readstring(virp, 1, TRUE); + return viminfo_readline(virp); +} + +void write_viminfo_sub_string(FILE *fp) +{ + if (get_viminfo_parameter('/') != 0 && old_sub != NULL) { + fputs(_("\n# Last Substitute String:\n$"), fp); + viminfo_writestring(fp, old_sub); + } +} + +/* + * Structure used for reading from the viminfo file. + */ +typedef struct { + char_u *vir_line; /* text of the current line */ + FILE *vir_fd; /* file descriptor */ + vimconv_T vir_conv; /* encoding conversion */ +} vir_T; +#endif diff --git a/src/nvim/shada.h b/src/nvim/shada.h new file mode 100644 index 0000000000..f0935796d7 --- /dev/null +++ b/src/nvim/shada.h @@ -0,0 +1,18 @@ +#ifndef NVIM_SHADA_H +#define NVIM_SHADA_H + +typedef long ShadaPosition; + +/// Flags for shada_read_file and children +enum { + kShaDaWantInfo = 1, ///< Load non-mark information + kShaDaWantMarks = 2, ///< Load file marks + kShaDaForceit = 4, ///< Overwrite info already read + kShaDaGetOldfiles = 8, ///< Load v:oldfiles. +}; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "shada.h.generated.h" +#endif + +#endif // NVIM_SHADA_H diff --git a/src/nvim/tag.c b/src/nvim/tag.c index 75055cc21a..b0d1a17c89 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -130,7 +130,7 @@ static char_u *tagmatchname = NULL; /* name of last used tag */ * Tag for preview window is remembered separately, to avoid messing up the * normal tagstack. */ -static taggy_T ptag_entry = {NULL, {INIT_POS_T(0, 0, 0), 0}, 0, 0}; +static taggy_T ptag_entry = {NULL, {INIT_POS_T(0, 0, 0), 0, 0, NULL}, 0, 0}; /* * Jump to tag; handling of tag commands and tag stack diff --git a/src/nvim/undo.c b/src/nvim/undo.c index 063c13084d..a04e7c2763 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -111,6 +111,7 @@ #include "nvim/types.h" #include "nvim/os/os.h" #include "nvim/os/time.h" +#include "nvim/api/private/helpers.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "undo.c.generated.h" @@ -325,6 +326,17 @@ static long get_undolevel(void) return curbuf->b_p_ul; } +static inline void zero_fmark_additional_data(fmark_T *fmarks) +{ + for (size_t i = 0; i < NMARKS; i++) { + if (fmarks[i].additional_data != NULL) { + api_free_dictionary(*fmarks[i].additional_data); + free(fmarks[i].additional_data); + fmarks[i].additional_data = NULL; + } + } +} + /* * Common code for various ways to save text before a change. * "top" is the line above the first changed line. @@ -467,7 +479,9 @@ int u_savecommon(linenr_T top, linenr_T bot, linenr_T newbot, int reload) ((curbuf->b_ml.ml_flags & ML_EMPTY) ? UH_EMPTYBUF : 0); /* save named marks and Visual marks for undo */ - memmove(uhp->uh_namedm, curbuf->b_namedm, sizeof(pos_T) * NMARKS); + zero_fmark_additional_data(curbuf->b_namedm); + memmove(uhp->uh_namedm, curbuf->b_namedm, + sizeof(curbuf->b_namedm[0]) * NMARKS); uhp->uh_visual = curbuf->b_visual; curbuf->b_u_newhead = uhp; @@ -785,7 +799,7 @@ static bool serialize_uhp(bufinfo_T *bi, u_header_T *uhp) undo_write_bytes(bi, (uintmax_t)uhp->uh_flags, 2); // Assume NMARKS will stay the same. for (size_t i = 0; i < (size_t)NMARKS; i++) { - serialize_pos(bi, uhp->uh_namedm[i]); + serialize_pos(bi, uhp->uh_namedm[i].mark); } serialize_visualinfo(bi, &uhp->uh_visual); uint8_t time_buf[8]; @@ -832,7 +846,9 @@ static u_header_T *unserialize_uhp(bufinfo_T *bi, char_u *file_name) uhp->uh_cursor_vcol = undo_read_4c(bi); uhp->uh_flags = undo_read_2c(bi); for (size_t i = 0; i < (size_t)NMARKS; i++) { - unserialize_pos(bi, &uhp->uh_namedm[i]); + unserialize_pos(bi, &uhp->uh_namedm[i].mark); + uhp->uh_namedm[i].timestamp = 0; + uhp->uh_namedm[i].fnum = 0; } unserialize_visualinfo(bi, &uhp->uh_visual); uhp->uh_time = undo_read_time(bi); @@ -2009,7 +2025,7 @@ static void u_undoredo(int undo) u_entry_T *newlist = NULL; int old_flags; int new_flags; - pos_T namedm[NMARKS]; + fmark_T namedm[NMARKS]; visualinfo_T visualinfo; int empty_buffer; /* buffer became empty */ u_header_T *curhead = curbuf->b_u_curhead; @@ -2029,7 +2045,8 @@ static void u_undoredo(int undo) /* * save marks before undo/redo */ - memmove(namedm, curbuf->b_namedm, sizeof(pos_T) * NMARKS); + zero_fmark_additional_data(curbuf->b_namedm); + memmove(namedm, curbuf->b_namedm, sizeof(curbuf->b_namedm[0]) * NMARKS); visualinfo = curbuf->b_visual; curbuf->b_op_start.lnum = curbuf->b_ml.ml_line_count; curbuf->b_op_start.col = 0; @@ -2158,7 +2175,8 @@ static void u_undoredo(int undo) * restore marks from before undo/redo */ for (i = 0; i < NMARKS; ++i) - if (curhead->uh_namedm[i].lnum != 0) { + if (curhead->uh_namedm[i].mark.lnum != 0) { + free_fmark(curbuf->b_namedm[i]); curbuf->b_namedm[i] = curhead->uh_namedm[i]; curhead->uh_namedm[i] = namedm[i]; } diff --git a/src/nvim/undo_defs.h b/src/nvim/undo_defs.h index 610adb4367..d841210815 100644 --- a/src/nvim/undo_defs.h +++ b/src/nvim/undo_defs.h @@ -5,6 +5,7 @@ #include "nvim/pos.h" #include "nvim/buffer_defs.h" +#include "nvim/mark_defs.h" /* Structure to store info about the Visual area. */ typedef struct { @@ -54,7 +55,7 @@ struct u_header { pos_T uh_cursor; /* cursor position before saving */ long uh_cursor_vcol; int uh_flags; /* see below */ - pos_T uh_namedm[NMARKS]; /* marks before undo/after redo */ + fmark_T uh_namedm[NMARKS]; /* marks before undo/after redo */ visualinfo_T uh_visual; /* Visual areas before undo/after redo */ time_t uh_time; /* timestamp when the change was made */ long uh_save_nr; /* set when the file was saved after the diff --git a/src/nvim/window.c b/src/nvim/window.c index 992463a8fc..b71b2cb603 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -1925,7 +1925,7 @@ int win_close(win_T *win, int free_buf) && (last_window() || curtab != prev_curtab || close_last_window_tabpage(win, free_buf, prev_curtab))) { /* Autocommands have close all windows, quit now. Restore - * curwin->w_buffer, otherwise writing viminfo may fail. */ + * curwin->w_buffer, otherwise writing ShaDa file may fail. */ if (curwin->w_buffer == NULL) curwin->w_buffer = curbuf; getout(0); diff --git a/test/functional/ex_getln/history_spec.lua b/test/functional/ex_getln/history_spec.lua new file mode 100644 index 0000000000..c3ef56c8ad --- /dev/null +++ b/test/functional/ex_getln/history_spec.lua @@ -0,0 +1,40 @@ +local helpers = require('test.functional.helpers') +local clear, nvim, call, eq = + helpers.clear, helpers.nvim, helpers.call, helpers.eq + +describe('history support code', function() + before_each(clear) + + local histadd = function(...) return call('histadd', ...) end + local histget = function(...) return call('histget', ...) end + local histdel = function(...) return call('histdel', ...) end + + it('correctly clears start of the history', function() + -- Regression test: check absense of the memory leak when clearing start of + -- the history using ex_getln.c/clr_history(). + eq(1, histadd(':', 'foo')) + eq(1, histdel(':')) + eq('', histget(':', -1)) + end) + + it('correctly clears end of the history', function() + -- Regression test: check absense of the memory leak when clearing end of + -- the history using ex_getln.c/clr_history(). + nvim('set_option', 'history', 1) + eq(1, histadd(':', 'foo')) + eq(1, histdel(':')) + eq('', histget(':', -1)) + end) + + it('correctly removes item from history', function() + -- Regression test: check that ex_getln.c/del_history_idx() correctly clears + -- history index after removing history entry. If it does not then deleting + -- history will result in a double free. + eq(1, histadd(':', 'foo')) + eq(1, histadd(':', 'bar')) + eq(1, histadd(':', 'baz')) + eq(1, histdel(':', -2)) + eq(1, histdel(':')) + eq('', histget(':', -1)) + end) +end) |