diff options
Diffstat (limited to 'src')
47 files changed, 5986 insertions, 2158 deletions
diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 0485fbacd2..7a0b5191d7 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -597,7 +597,7 @@ static void init_type_metadata(Dictionary *metadata) } /// Creates a deep clone of an object -static Object copy_object(Object obj) +Object copy_object(Object obj) { switch (obj.type) { case kObjectTypeNil: 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..b3eba4f5f6 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,9 +556,21 @@ static void free_buffer(buf_T *buf) free_buffer_stuff(buf, TRUE); unref_var_dict(buf->b_vars); aubuflocal_remove(buf); + dict_unref(buf->additional_data); + clear_fmark(&buf->b_last_cursor); + clear_fmark(&buf->b_last_insert); + clear_fmark(&buf->b_last_change); + for (size_t i = 0; i < NMARKS; i++) { + free_fmark(buf->b_namedm[i]); + } + for (int i = 0; i < buf->b_changelistlen; i++) { + free_fmark(buf->b_changelist[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. + memset(&buf->b_namedm[0], 0, sizeof(buf->b_namedm)); + memset(&buf->b_changelist[0], 0, sizeof(buf->b_changelist)); buf->b_next = au_pending_free_buf; au_pending_free_buf = buf; } else { @@ -1978,12 +1991,18 @@ buflist_nr2name ( fullname ? buf->b_ffname : buf->b_fname); } -/* - * Set the "lnum" and "col" for the buffer "buf" and the current window. - * When "copy_options" is TRUE save the local window option values. - * When "lnum" is 0 only do the options. - */ -static void buflist_setfpos(buf_T *buf, win_T *win, linenr_T lnum, colnr_T col, int copy_options) +/// Set the line and column numbers for the given buffer and window +/// +/// @param[in,out] buf Buffer for which line and column are set. +/// @param[in,out] win Window for which line and column are set. +/// @param[in] lnum Line number to be set. If it is zero then only +/// options are touched. +/// @param[in] col Column number to be set. +/// @param[in] copy_options If true save the local window option values. +void buflist_setfpos(buf_T *const buf, win_T *const win, + linenr_T lnum, colnr_T col, + bool copy_options) + FUNC_ATTR_NONNULL_ALL { wininfo_T *wip; @@ -4164,93 +4183,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..3eabb7ee43 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,21 +506,21 @@ 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 - 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_cursor; // cursor position when last unloading this + // buffer + 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 */ - pos_T b_changelist[JUMPLISTSIZE]; + fmark_T b_changelist[JUMPLISTSIZE]; int b_changelistlen; /* number of active entries */ bool b_new_change; /* set by u_savecommon() */ @@ -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. @@ -757,6 +748,8 @@ struct file_buffer { signlist_T *b_signlist; /* list of signs to draw */ Terminal *terminal; // Terminal instance associated with the buffer + + dict_T *additional_data; // Additional data from shada file if any. }; /* 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..d02348028a 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -108,18 +108,6 @@ function/variable names. */ /* - * In a hashtab item "hi_key" points to "di_key" in a dictitem. - * This avoids adding a pointer to the hashtab item. - * DI2HIKEY() converts a dictitem pointer to a hashitem key pointer. - * HIKEY2DI() converts a hashitem key pointer to a dictitem pointer. - * HI2DI() converts a hashitem pointer to a dictitem pointer. - */ -static dictitem_T dumdi; -#define DI2HIKEY(di) ((di)->di_key) -#define HIKEY2DI(p) ((dictitem_T *)(p - (dumdi.di_key - (char_u *)&dumdi))) -#define HI2DI(hi) HIKEY2DI((hi)->hi_key) - -/* * Structure returned by get_lval() and used by set_var_lval(). * For a plain name: * "name" points to the variable name. @@ -354,7 +342,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: */ @@ -5352,7 +5340,7 @@ static int list_concat(list_T *l1, list_T *l2, typval_T *tv) return FAIL; /* make a copy of the first list. */ - l = list_copy(l1, FALSE, 0); + l = list_copy(NULL, l1, false, 0); if (l == NULL) return FAIL; tv->v_type = VAR_LIST; @@ -5363,13 +5351,20 @@ static int list_concat(list_T *l1, list_T *l2, typval_T *tv) return OK; } -/* - * Make a copy of list "orig". Shallow if "deep" is FALSE. - * The refcount of the new list is set to 1. - * See item_copy() for "copyID". - * Returns NULL if orig is NULL or some failure happens. - */ -static list_T *list_copy(list_T *orig, int deep, int copyID) +/// Make a copy of list +/// +/// @param[in] conv If non-NULL, then all internal strings will be converted. +/// @param[in] orig Original list to copy. +/// @param[in] deep If false, then shallow copy will be done. +/// @param[in] copyID See var_item_copy(). +/// +/// @return Copied list. May be NULL in case original list is NULL or some +/// failure happens. The refcount of the new list is set to 1. +static list_T *list_copy(const vimconv_T *const conv, + list_T *const orig, + const bool deep, + const int copyID) + FUNC_ATTR_WARN_UNUSED_RESULT { listitem_T *item; listitem_T *ni; @@ -5388,7 +5383,7 @@ static list_T *list_copy(list_T *orig, int deep, int copyID) item = item->li_next) { ni = listitem_alloc(); if (deep) { - if (item_copy(&item->li_tv, &ni->li_tv, deep, copyID) == FAIL) { + if (var_item_copy(conv, &item->li_tv, &ni->li_tv, deep, copyID) == FAIL) { xfree(ni); break; } @@ -5546,6 +5541,7 @@ static int list_join(garray_T *const gap, list_T *const l, bool garbage_collect(void) { bool abort = false; +#define ABORTING(func) abort = abort || func // Only do this once. want_garbage_collect = false; @@ -5564,45 +5560,117 @@ bool garbage_collect(void) // referenced through previous_funccal. This must be first, because if // the item is referenced elsewhere the funccal must not be freed. for (funccall_T *fc = previous_funccal; fc != NULL; fc = fc->caller) { - abort = abort || set_ref_in_ht(&fc->l_vars.dv_hashtab, copyID + 1, NULL); - abort = abort || set_ref_in_ht(&fc->l_avars.dv_hashtab, copyID + 1, NULL); + ABORTING(set_ref_in_ht)(&fc->l_vars.dv_hashtab, copyID + 1, NULL); + ABORTING(set_ref_in_ht)(&fc->l_avars.dv_hashtab, copyID + 1, NULL); } // script-local variables for (int i = 1; i <= ga_scripts.ga_len; ++i) { - abort = abort || set_ref_in_ht(&SCRIPT_VARS(i), copyID, NULL); + ABORTING(set_ref_in_ht)(&SCRIPT_VARS(i), copyID, NULL); } - // buffer-local variables FOR_ALL_BUFFERS(buf) { - abort = abort || set_ref_in_item(&buf->b_bufvar.di_tv, copyID, NULL, NULL); + // buffer-local variables + ABORTING(set_ref_in_item)(&buf->b_bufvar.di_tv, copyID, NULL, NULL); + // buffer marks (ShaDa additional data) + ABORTING(set_ref_in_fmark)(buf->b_last_cursor, copyID); + ABORTING(set_ref_in_fmark)(buf->b_last_insert, copyID); + ABORTING(set_ref_in_fmark)(buf->b_last_change, copyID); + for (size_t i = 0; i < NMARKS; i++) { + ABORTING(set_ref_in_fmark)(buf->b_namedm[i], copyID); + } + // buffer change list (ShaDa additional data) + for (int i = 0; i < buf->b_changelistlen; i++) { + ABORTING(set_ref_in_fmark)(buf->b_changelist[i], copyID); + } + // buffer ShaDa additional data + ABORTING(set_ref_dict)(buf->additional_data, copyID); } - // window-local variables FOR_ALL_TAB_WINDOWS(tp, wp) { - abort = abort || set_ref_in_item(&wp->w_winvar.di_tv, copyID, NULL, NULL); + // window-local variables + ABORTING(set_ref_in_item)(&wp->w_winvar.di_tv, copyID, NULL, NULL); + // window jump list (ShaDa additional data) + for (int i = 0; i < wp->w_jumplistlen; i++) { + ABORTING(set_ref_in_fmark)(wp->w_jumplist[i].fmark, copyID); + } } if (aucmd_win != NULL) { - abort = abort || - set_ref_in_item(&aucmd_win->w_winvar.di_tv, copyID, NULL, NULL); + ABORTING(set_ref_in_item)(&aucmd_win->w_winvar.di_tv, copyID, NULL, NULL); + } + + // registers (ShaDa additional data) + { + const void *reg_iter = NULL; + do { + yankreg_T reg; + char name = NUL; + reg_iter = op_register_iter(reg_iter, &name, ®); + if (name != NUL) { + ABORTING(set_ref_dict)(reg.additional_data, copyID); + } + } while (reg_iter != NULL); + } + + // global marks (ShaDa additional data) + { + const void *mark_iter = NULL; + do { + xfmark_T fm; + char name = NUL; + mark_iter = mark_global_iter(mark_iter, &name, &fm); + if (name != NUL) { + ABORTING(set_ref_dict)(fm.fmark.additional_data, copyID); + } + } while (mark_iter != NULL); } // tabpage-local variables FOR_ALL_TABS(tp) { - abort = abort || set_ref_in_item(&tp->tp_winvar.di_tv, copyID, NULL, NULL); + ABORTING(set_ref_in_item)(&tp->tp_winvar.di_tv, copyID, NULL, NULL); } // global variables - abort = abort || set_ref_in_ht(&globvarht, copyID, NULL); + ABORTING(set_ref_in_ht)(&globvarht, copyID, NULL); // function-local variables for (funccall_T *fc = current_funccal; fc != NULL; fc = fc->caller) { - abort = abort || set_ref_in_ht(&fc->l_vars.dv_hashtab, copyID, NULL); - abort = abort || set_ref_in_ht(&fc->l_avars.dv_hashtab, copyID, NULL); + ABORTING(set_ref_in_ht)(&fc->l_vars.dv_hashtab, copyID, NULL); + ABORTING(set_ref_in_ht)(&fc->l_avars.dv_hashtab, copyID, NULL); } // v: vars - abort = abort || set_ref_in_ht(&vimvarht, copyID, NULL); + ABORTING(set_ref_in_ht)(&vimvarht, copyID, NULL); + + // history items (ShaDa additional elements) + if (p_hi) { + for (uint8_t i = 0; i < HIST_COUNT; i++) { + const void *iter = NULL; + do { + histentry_T hist; + iter = hist_iter(iter, i, false, &hist); + if (hist.hisstr != NULL) { + ABORTING(set_ref_list)(hist.additional_elements, copyID); + } + } while (iter != NULL); + } + } + + // previously used search/substitute patterns (ShaDa additional data) + { + SearchPattern pat; + get_search_pattern(&pat); + ABORTING(set_ref_dict)(pat.additional_data, copyID); + get_substitute_pattern(&pat); + ABORTING(set_ref_dict)(pat.additional_data, copyID); + } + + // previously used replacement string + { + SubReplacementString sub; + sub_get_replacement(&sub); + ABORTING(set_ref_list)(sub.additional_elements, copyID); + } bool did_free = false; if (!abort) { @@ -5631,6 +5699,7 @@ bool garbage_collect(void) verb_msg((char_u *)_( "Not enough memory to set references, garbage collection aborted!")); } +#undef ABORTING return did_free; } @@ -5690,6 +5759,7 @@ static int free_unref_items(int copyID) /// /// @returns true if setting references failed somehow. bool set_ref_in_ht(hashtab_T *ht, int copyID, list_stack_T **list_stack) + FUNC_ATTR_WARN_UNUSED_RESULT { bool abort = false; ht_stack_T *ht_stack = NULL; @@ -5732,6 +5802,7 @@ bool set_ref_in_ht(hashtab_T *ht, int copyID, list_stack_T **list_stack) /// /// @returns true if setting references failed somehow. bool set_ref_in_list(list_T *l, int copyID, ht_stack_T **ht_stack) + FUNC_ATTR_WARN_UNUSED_RESULT { bool abort = false; list_stack_T *list_stack = NULL; @@ -5772,6 +5843,7 @@ bool set_ref_in_list(list_T *l, int copyID, ht_stack_T **ht_stack) /// @returns true if setting references failed somehow. bool set_ref_in_item(typval_T *tv, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack) + FUNC_ATTR_WARN_UNUSED_RESULT { bool abort = false; @@ -5821,6 +5893,52 @@ bool set_ref_in_item(typval_T *tv, int copyID, ht_stack_T **ht_stack, return abort; } +/// Mark all lists and dicts referenced in given mark +/// +/// @returns true if setting references failed somehow. +static inline bool set_ref_in_fmark(fmark_T fm, int copyID) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (fm.additional_data != NULL + && fm.additional_data->dv_copyID != copyID) { + fm.additional_data->dv_copyID = copyID; + return set_ref_in_ht(&fm.additional_data->dv_hashtab, copyID, NULL); + } + return false; +} + +/// Mark all lists and dicts referenced in given list and the list itself +/// +/// @returns true if setting references failed somehow. +static inline bool set_ref_list(list_T *list, int copyID) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (list != NULL) { + typval_T tv = (typval_T) { + .v_type = VAR_LIST, + .vval = { .v_list = list } + }; + return set_ref_in_item(&tv, copyID, NULL, NULL); + } + return false; +} + +/// Mark all lists and dicts referenced in given dict and the dict itself +/// +/// @returns true if setting references failed somehow. +static inline bool set_ref_dict(dict_T *dict, int copyID) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (dict != NULL) { + typval_T tv = (typval_T) { + .v_type = VAR_DICT, + .vval = { .v_dict = dict } + }; + return set_ref_in_item(&tv, copyID, NULL, NULL); + } + return false; +} + /* * Allocate an empty header for a dictionary. */ @@ -5964,13 +6082,20 @@ void dictitem_free(dictitem_T *item) xfree(item); } -/* - * Make a copy of dict "d". Shallow if "deep" is FALSE. - * The refcount of the new dict is set to 1. - * See item_copy() for "copyID". - * Returns NULL if orig is NULL or some other failure. - */ -static dict_T *dict_copy(dict_T *orig, int deep, int copyID) +/// Make a copy of dictionary +/// +/// @param[in] conv If non-NULL, then all internal strings will be converted. +/// @param[in] orig Original dictionary to copy. +/// @param[in] deep If false, then shallow copy will be done. +/// @param[in] copyID See var_item_copy(). +/// +/// @return Copied dictionary. May be NULL in case original dictionary is NULL +/// or some failure happens. The refcount of the new dictionary is set +/// to 1. +static dict_T *dict_copy(const vimconv_T *const conv, + dict_T *const orig, + const bool deep, + const int copyID) { dictitem_T *di; int todo; @@ -5990,10 +6115,21 @@ static dict_T *dict_copy(dict_T *orig, int deep, int copyID) if (!HASHITEM_EMPTY(hi)) { --todo; - di = dictitem_alloc(hi->hi_key); + if (conv == NULL || conv->vc_type == CONV_NONE) { + di = dictitem_alloc(hi->hi_key); + } else { + char *const key = (char *) string_convert((vimconv_T *) conv, + hi->hi_key, NULL); + if (key == NULL) { + di = dictitem_alloc(hi->hi_key); + } else { + di = dictitem_alloc((char_u *) key); + xfree(key); + } + } if (deep) { - if (item_copy(&HI2DI(hi)->di_tv, &di->di_tv, deep, - copyID) == FAIL) { + if (var_item_copy(conv, &HI2DI(hi)->di_tv, &di->di_tv, deep, + copyID) == FAIL) { xfree(di); break; } @@ -6305,7 +6441,7 @@ failret: /// the results. /// @param firstargname Name of the first argument. /// @param name Name of the target converter. -#define DEFINE_VIML_CONV_FUNCTIONS(name, firstargtype, firstargname) \ +#define DEFINE_VIML_CONV_FUNCTIONS(scope, name, firstargtype, firstargname) \ static int name##_convert_one_value(firstargtype firstargname, \ MPConvStack *const mpstack, \ typval_T *const tv, \ @@ -6543,7 +6679,7 @@ name##_convert_one_value_regular_dict: \ return OK; \ } \ \ -static int vim_to_##name(firstargtype firstargname, typval_T *const tv) \ +scope int vim_to_##name(firstargtype firstargname, typval_T *const tv) \ FUNC_ATTR_WARN_UNUSED_RESULT \ { \ current_copyID += COPYID_INC; \ @@ -6739,7 +6875,7 @@ vim_to_msgpack_error_ret: \ #define CONV_ALLOW_SPECIAL false -DEFINE_VIML_CONV_FUNCTIONS(string, garray_T *const, gap) +DEFINE_VIML_CONV_FUNCTIONS(static, string, garray_T *const, gap) #undef CONV_RECURSE #define CONV_RECURSE(val, conv_type) \ @@ -6769,7 +6905,7 @@ DEFINE_VIML_CONV_FUNCTIONS(string, garray_T *const, gap) return OK; \ } while (0) -DEFINE_VIML_CONV_FUNCTIONS(echo, garray_T *const, gap) +DEFINE_VIML_CONV_FUNCTIONS(static, echo, garray_T *const, gap) #undef CONV_STRING #undef CONV_STR_STRING @@ -8344,7 +8480,7 @@ static void f_confirm(typval_T *argvars, typval_T *rettv) */ static void f_copy(typval_T *argvars, typval_T *rettv) { - item_copy(&argvars[0], rettv, FALSE, 0); + var_item_copy(NULL, &argvars[0], rettv, false, 0); } /* @@ -8513,7 +8649,9 @@ static void f_deepcopy(typval_T *argvars, typval_T *rettv) EMSG(_(e_invarg)); else { current_copyID += COPYID_INC; - item_copy(&argvars[0], rettv, TRUE, noref == 0 ? current_copyID : 0); + var_item_copy(NULL, &argvars[0], rettv, true, (noref == 0 + ? current_copyID + : 0)); } } @@ -10482,6 +10620,7 @@ static void f_has(typval_T *argvars, typval_T *rettv) "scrollbind", "showcmd", "cmdline_info", + "shada", "signs", "smartindent", "startuptime", @@ -10498,7 +10637,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", @@ -12486,7 +12624,7 @@ static inline bool vim_list_to_buf(const list_T *const list, #define CONV_ALLOW_SPECIAL true -DEFINE_VIML_CONV_FUNCTIONS(msgpack, msgpack_packer *const, packer) +DEFINE_VIML_CONV_FUNCTIONS(, msgpack, msgpack_packer *const, packer) #undef CONV_STRING #undef CONV_STR_STRING @@ -12592,7 +12730,7 @@ static inline ListReaderState init_lrstate(const list_T *const list) } /// Convert msgpack object to a VimL one -static int msgpack_to_vim(const msgpack_object mobj, typval_T *const rettv) +int msgpack_to_vim(const msgpack_object mobj, typval_T *const rettv) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { #define INIT_SPECIAL_DICT(tv, type, val) \ @@ -17353,14 +17491,14 @@ char_u *get_vim_var_str(int idx) FUNC_ATTR_PURE FUNC_ATTR_NONNULL_RET * Get List v: variable value. Caller must take care of reference count when * needed. */ -list_T *get_vim_var_list(int idx) FUNC_ATTR_PURE FUNC_ATTR_NONNULL_RET +list_T *get_vim_var_list(int idx) FUNC_ATTR_PURE { return vimvars[idx].vv_list; } /// Get Dictionary v: variable value. Caller must take care of reference count /// when needed. -dict_T *get_vim_var_dict(int idx) FUNC_ATTR_PURE FUNC_ATTR_NONNULL_RET +dict_T *get_vim_var_dict(int idx) FUNC_ATTR_PURE { return vimvars[idx].vv_dict; } @@ -18446,14 +18584,28 @@ void copy_tv(typval_T *from, typval_T *to) } } -/* - * Make a copy of an item. - * Lists and Dictionaries are also copied. A deep copy if "deep" is set. - * For deepcopy() "copyID" is zero for a full copy or the ID for when a - * reference to an already copied list/dict can be used. - * Returns FAIL or OK. - */ -static int item_copy(typval_T *from, typval_T *to, int deep, int copyID) +/// Make a copy of an item +/// +/// Lists and Dictionaries are also copied. +/// +/// @param[in] conv If not NULL, convert all copied strings. +/// @param[in] from Value to copy. +/// @param[out] to Location where to copy to. +/// @param[in] deep If true, use copy the container and all of the contained +/// containers (nested). +/// @param[in] copyID If non-zero then when container is referenced more then +/// once then copy of it that was already done is used. E.g. +/// when copying list `list = [list2, list2]` (`list[0] is +/// list[1]`) var_item_copy with zero copyID will emit +/// a copy with (`copy[0] isnot copy[1]`), with non-zero it +/// will emit a copy with (`copy[0] is copy[1]`) like in the +/// original list. Not use when deep is false. +int var_item_copy(const vimconv_T *const conv, + typval_T *const from, + typval_T *const to, + const bool deep, + const int copyID) + FUNC_ATTR_NONNULL_ARG(2, 3) { static int recurse = 0; int ret = OK; @@ -18467,10 +18619,23 @@ static int item_copy(typval_T *from, typval_T *to, int deep, int copyID) switch (from->v_type) { case VAR_NUMBER: case VAR_FLOAT: - case VAR_STRING: case VAR_FUNC: copy_tv(from, to); break; + case VAR_STRING: + if (conv == NULL || conv->vc_type == CONV_NONE) { + copy_tv(from, to); + } else { + to->v_type = VAR_STRING; + to->v_lock = 0; + if ((to->vval.v_string = string_convert((vimconv_T *)conv, + from->vval.v_string, + NULL)) + == NULL) { + to->vval.v_string = (char_u *) xstrdup((char *) from->vval.v_string); + } + } + break; case VAR_LIST: to->v_type = VAR_LIST; to->v_lock = 0; @@ -18480,8 +18645,9 @@ static int item_copy(typval_T *from, typval_T *to, int deep, int copyID) /* use the copy made earlier */ to->vval.v_list = from->vval.v_list->lv_copylist; ++to->vval.v_list->lv_refcount; - } else - to->vval.v_list = list_copy(from->vval.v_list, deep, copyID); + } else { + to->vval.v_list = list_copy(conv, from->vval.v_list, deep, copyID); + } if (to->vval.v_list == NULL) ret = FAIL; break; @@ -18494,13 +18660,14 @@ static int item_copy(typval_T *from, typval_T *to, int deep, int copyID) /* use the copy made earlier */ to->vval.v_dict = from->vval.v_dict->dv_copydict; ++to->vval.v_dict->dv_refcount; - } else - to->vval.v_dict = dict_copy(from->vval.v_dict, deep, copyID); + } else { + to->vval.v_dict = dict_copy(conv, from->vval.v_dict, deep, copyID); + } if (to->vval.v_dict == NULL) ret = FAIL; break; default: - EMSG2(_(e_intern2), "item_copy()"); + EMSG2(_(e_intern2), "var_item_copy()"); ret = FAIL; } --recurse; @@ -20710,109 +20877,64 @@ static var_flavour_T var_flavour(char_u *varname) if (ASCII_ISUPPER(*p)) { while (*(++p)) - if (ASCII_ISLOWER(*p)) + if (ASCII_ISLOWER(*p)) { return VAR_FLAVOUR_SESSION; - return VAR_FLAVOUR_VIMINFO; - } else + } + 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_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 ((size_t) (hi - hifirst) < hinum + && (HASHITEM_EMPTY(hi) + || var_flavour(HI2DI(hi)->di_key) != VAR_FLAVOUR_SHADA)) { + hi++; + } + if ((size_t) (hi - hifirst) == hinum) { + return NULL; + } + } else { + hi = (const hashitem_T *) iter; + } + *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 viminfo_readline(virp); + 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/eval.h b/src/nvim/eval.h index 8f065eda33..864daed716 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -1,6 +1,8 @@ #ifndef NVIM_EVAL_H #define NVIM_EVAL_H +#include <msgpack.h> + #include "nvim/profile.h" /* Defines for Vim variables. These must match vimvars[] in eval.c! */ @@ -72,6 +74,8 @@ enum { /// Maximum number of function arguments #define MAX_FUNC_ARGS 20 +int vim_to_msgpack(msgpack_packer *const, typval_T *const); + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "eval.h.generated.h" #endif diff --git a/src/nvim/eval_defs.h b/src/nvim/eval_defs.h index a8a8acd048..373f1e6278 100644 --- a/src/nvim/eval_defs.h +++ b/src/nvim/eval_defs.h @@ -2,6 +2,7 @@ #define NVIM_EVAL_DEFS_H #include <limits.h> +#include <stddef.h> #include "nvim/hashtab.h" @@ -132,4 +133,16 @@ typedef struct list_stack_S { struct list_stack_S *prev; } list_stack_T; +// In a hashtab item "hi_key" points to "di_key" in a dictitem. +// This avoids adding a pointer to the hashtab item. + +/// Convert a dictitem pointer to a hashitem key pointer +#define DI2HIKEY(di) ((di)->di_key) + +/// Convert a hashitem key pointer to a dictitem pointer +#define HIKEY2DI(p) ((dictitem_T *)(p - offsetof(dictitem_T, di_key))) + +/// Convert a hashitem pointer to a dictitem pointer +#define HI2DI(hi) HIKEY2DI((hi)->hi_key) + #endif // NVIM_EVAL_DEFS_H diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 81abf2fa63..5db3880026 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -67,6 +67,7 @@ #include "nvim/os/os.h" #include "nvim/os/shell.h" #include "nvim/os/input.h" +#include "nvim/os/time.h" /* * Struct to hold the sign properties. @@ -1391,550 +1392,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 +2821,33 @@ 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) { + list_unref(old_sub.additional_elements); + } + old_sub = sub; +} /* do_sub() * @@ -3473,16 +2955,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 +3986,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..721145efd8 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/eval_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. + list_T *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_cmds.lua b/src/nvim/ex_cmds.lua index 5221554306..77f7dba81b 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -2120,6 +2120,12 @@ return { func='ex_wrongmodifier', }, { + command='rshada', + flags=bit.bor(BANG, FILE1, TRLBAR, CMDWIN), + addr_type=ADDR_LINES, + func='ex_shada', + }, + { command='runtime', flags=bit.bor(BANG, NEEDARG, FILES, TRLBAR, SBOXOK, CMDWIN), addr_type=ADDR_LINES, @@ -2153,7 +2159,7 @@ return { command='rviminfo', flags=bit.bor(BANG, FILE1, TRLBAR, CMDWIN), addr_type=ADDR_LINES, - func='ex_viminfo', + func='ex_shada', }, { command='substitute', @@ -3032,6 +3038,12 @@ return { func='ex_wsverb', }, { + command='wshada', + flags=bit.bor(BANG, FILE1, TRLBAR, CMDWIN), + addr_type=ADDR_LINES, + func='ex_shada', + }, + { command='wundo', flags=bit.bor(BANG, NEEDARG, FILE1), addr_type=ADDR_LINES, @@ -3041,7 +3053,7 @@ return { command='wviminfo', flags=bit.bor(BANG, FILE1, TRLBAR, CMDWIN), addr_type=ADDR_LINES, - func='ex_viminfo', + func='ex_shada', }, { command='xit', diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index f7162896ff..a9262ca6ea 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; @@ -9139,22 +9140,21 @@ int put_line(FILE *fd, char *s) } /* - * ":rviminfo" and ":wviminfo". + * ":rshada" and ":wshada". */ -static void ex_viminfo(exarg_T *eap) +static void ex_shada(exarg_T *eap) { - char_u *save_viminfo; + char_u *save_shada; - save_viminfo = p_viminfo; - 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); - p_viminfo = save_viminfo; + save_shada = p_shada; + if (*p_shada == NUL) + p_shada = (char_u *)"'100"; + if (eap->cmdidx == CMD_rviminfo || eap->cmdidx == CMD_rshada) { + (void) shada_read_everything((char *) eap->arg, eap->forceit, false); + } else { + shada_write_file((char *) eap->arg, eap->forceit); + } + p_shada = save_shada; } /* diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 9739090d7c..24e31b1ed7 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -65,6 +65,7 @@ #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/event/loop.h" +#include "nvim/os/time.h" /* * Variables shared between getcmdline(), redrawcmdline() and others. @@ -100,12 +101,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 +4225,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 +4246,18 @@ 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 +{ + xfree(hisptr->hisstr); + list_unref(hisptr->additional_elements); + clear_hist_entry(hisptr); +} + +static inline void clear_hist_entry(histentry_T *hisptr) + FUNC_ATTR_NONNULL_ALL { - hisptr->hisnum = 0; - hisptr->viminfo = FALSE; - hisptr->hisstr = NULL; + memset(hisptr, 0, sizeof(*hisptr)); } /* @@ -4268,9 +4268,8 @@ static int in_history ( int type, char_u *str, - int move_to_front, /* Move the entry to the front if it exists */ - int sep, - int writing /* ignore entries read from viminfo */ + int move_to_front, // Move the entry to the front if it exists + int sep ) { int i; @@ -4288,7 +4287,6 @@ in_history ( * well. */ p = history[type][i].hisstr; if (STRCMP(str, p) == 0 - && !(writing && history[type][i].viminfo) && (type != HIST_SEARCH || sep == p[STRLEN(p) + 1])) { if (!move_to_front) return TRUE; @@ -4300,6 +4298,7 @@ in_history ( } while (i != hisidx[type]); if (last_i >= 0) { + list_T *const list = history[type][i].additional_elements; str = history[type][i].hisstr; while (i != hisidx[type]) { if (++i >= hislen) @@ -4307,12 +4306,14 @@ in_history ( history[type][last_i] = history[type][i]; last_i = i; } + list_unref(list); history[type][i].hisnum = ++hisnum[type]; - history[type][i].viminfo = FALSE; history[type][i].hisstr = str; - return TRUE; + history[type][i].timestamp = os_time(); + history[type][i].additional_elements = NULL; + return true; } - return FALSE; + return false; } /* @@ -4372,27 +4373,27 @@ 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; } last_maptick = -1; } - if (!in_history(histype, new_entry, TRUE, sep, FALSE)) { + if (!in_history(histype, new_entry, true, sep)) { 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]; - hisptr->viminfo = FALSE; if (histype == HIST_SEARCH && in_map) last_maptick = maptick; } @@ -4545,23 +4546,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; @@ -4578,7 +4577,7 @@ int del_history_entry(int histype, char_u *str) int idx; int i; int last; - int found = FALSE; + bool found = false; regmatch.regprog = NULL; regmatch.rm_ic = FALSE; /* always match case */ @@ -4595,9 +4594,8 @@ int del_history_entry(int histype, char_u *str) if (hisptr->hisstr == NULL) break; if (vim_regexec(®match, hisptr->hisstr, (colnr_T)0)) { - found = TRUE; - xfree(hisptr->hisstr); - clear_hist_entry(hisptr); + found = true; + hist_free_entry(hisptr); } else { if (i != last) { history[histype][last] = *hisptr; @@ -4628,7 +4626,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 +4639,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; } @@ -4762,248 +4761,31 @@ 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 '/' */ -) +/// Translate a history type number to the associated character +int hist_type2char(int type) + FUNC_ATTR_CONST { - if (type == HIST_CMD) - return ':'; - if (type == HIST_SEARCH) { - if (use_question) - return '?'; - else + switch (type) { + case HIST_CMD: { + return ':'; + } + case HIST_SEARCH: { 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; + case HIST_EXPR: { + return '='; } - 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; + case HIST_INPUT: { + return '@'; } - idx += 1; - idx %= hislen; - for (i = 0; i < viminfo_hisidx[type]; i++) { - history[type][idx++].hisnum = ++hisnum[type]; - idx %= hislen; + case HIST_DEBUG: { + return '>'; } - 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; - } - } + default: { + assert(false); } - 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; } + return NUL; } /* @@ -5294,3 +5076,87 @@ 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 uint8_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); +} + +/// Get array of history items +/// +/// @param[in] history_type Type of the history to get array for. +/// @param[out] new_hisidx Location where last index in the new array should +/// be saved. +/// @param[out] new_hisnum Location where last history number in the new +/// history should be saved. +/// +/// @return Pointer to the array or NULL. +histentry_T *hist_get_array(const uint8_t history_type, int **const new_hisidx, + int **const new_hisnum) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + init_history(); + *new_hisidx = &(hisidx[history_type]); + *new_hisnum = &(hisnum[history_type]); + return history[history_type]; +} diff --git a/src/nvim/ex_getln.h b/src/nvim/ex_getln.h index 2b82f934d5..c537d681c6 100644 --- a/src/nvim/ex_getln.h +++ b/src/nvim/ex_getln.h @@ -1,6 +1,7 @@ #ifndef NVIM_EX_GETLN_H #define NVIM_EX_GETLN_H +#include "nvim/eval_defs.h" #include "nvim/ex_cmds.h" /* Values for nextwild() and ExpandOne(). See ExpandOne() for meaning. */ @@ -23,18 +24,28 @@ #define WILD_ESCAPE 128 #define WILD_ICASE 256 -/* - * There are four history tables: - */ -#define HIST_CMD 0 /* colon commands */ -#define HIST_SEARCH 1 /* search commands */ -#define HIST_EXPR 2 /* expressions (from entering = register) */ -#define HIST_INPUT 3 /* input() lines */ -#define HIST_DEBUG 4 /* debug commands */ -#define HIST_COUNT 5 /* number of history tables */ +/// Present history tables +typedef enum { + HIST_CMD, ///< Colon commands. + HIST_SEARCH, ///< Search commands. + HIST_EXPR, ///< Expressions (e.g. from entering = register). + HIST_INPUT, ///< input() lines. + HIST_DEBUG, ///< Debug commands. +} HistoryType; + +/// Number of history tables +#define HIST_COUNT (HIST_DEBUG + 1) typedef char_u *(*CompleteListItemGetter)(expand_T *, int); +/// History entry definition +typedef struct hist_entry { + int hisnum; ///< Entry identifier number. + char_u *hisstr; ///< Actual entry, separator char after the NUL. + Timestamp timestamp; ///< Time when entry was added. + list_T *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..a7472b40e2 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,16 +2167,17 @@ 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); + if (!curbuf->b_marks_read && get_shada_parameter('\'') > 0 + && curbuf->b_ffname != NULL) { + shada_read_marks(); + } - /* Always set b_marks_read; needed when 'viminfo' is changed to include + /* Always set b_marks_read; needed when 'shada' is changed to include * the ' parameter after opening a buffer. */ curbuf->b_marks_read = true; } diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 60d03cec0c..0ef0a12889 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -891,8 +891,8 @@ EXTERN int must_redraw INIT(= 0); /* type of redraw necessary */ 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 int need_highlight_changed INIT(= true); +EXTERN char *used_shada_file INIT(= NULL); // name of the ShaDa file to use #define NSCRIPT 15 EXTERN FILE *scriptin[NSCRIPT]; /* streams to read script from */ diff --git a/src/nvim/lib/khash.h b/src/nvim/lib/khash.h index 96e7ea6df0..56be29d14c 100644 --- a/src/nvim/lib/khash.h +++ b/src/nvim/lib/khash.h @@ -114,8 +114,8 @@ int main() { */ -#ifndef __AC_KHASH_H -#define __AC_KHASH_H +#ifndef NVIM_LIB_KHASH_H +#define NVIM_LIB_KHASH_H /*! @header @@ -194,166 +194,229 @@ static const double __ac_HASH_UPPER = 0.77; khval_t *vals; \ } kh_##name##_t; -#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ - extern kh_##name##_t *kh_init_##name(void); \ - extern void kh_destroy_##name(kh_##name##_t *h); \ - extern void kh_clear_##name(kh_##name##_t *h); \ - extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ - extern void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ - extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ - extern void kh_del_##name(kh_##name##_t *h, khint_t x); - -#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ - SCOPE kh_##name##_t *kh_init_##name(void) { \ - return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ - } \ - SCOPE void kh_destroy_##name(kh_##name##_t *h) \ - REAL_FATTR_UNUSED; \ - SCOPE void kh_destroy_##name(kh_##name##_t *h) \ - { \ - if (h) { \ - kfree((void *)h->keys); kfree(h->flags); \ - kfree((void *)h->vals); \ - kfree(h); \ - } \ - } \ - SCOPE void kh_clear_##name(kh_##name##_t *h) \ - REAL_FATTR_UNUSED; \ - SCOPE void kh_clear_##name(kh_##name##_t *h) \ - { \ - if (h && h->flags) { \ - memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ - h->size = h->n_occupied = 0; \ - } \ - } \ - SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ - REAL_FATTR_UNUSED; \ - SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ - { \ - if (h->n_buckets) { \ - khint_t k, i, last, mask, step = 0; \ - mask = h->n_buckets - 1; \ - k = __hash_func(key); i = k & mask; \ - last = i; \ - while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ - i = (i + (++step)) & mask; \ - if (i == last) return h->n_buckets; \ - } \ - return __ac_iseither(h->flags, i)? h->n_buckets : i; \ - } else return 0; \ - } \ - SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ - REAL_FATTR_UNUSED; \ - SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ - { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ - khint32_t *new_flags = 0; \ - khint_t j = 1; \ - { \ - kroundup32(new_n_buckets); \ - if (new_n_buckets < 4) new_n_buckets = 4; \ - if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ - else { /* hash table size to be changed (shrink or expand); rehash */ \ - new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ - memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ - if (h->n_buckets < new_n_buckets) { /* expand */ \ - khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ - h->keys = new_keys; \ - if (kh_is_map) { \ - khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ - h->vals = new_vals; \ - } \ - } /* otherwise shrink */ \ - } \ - } \ - if (j) { /* rehashing is needed */ \ - for (j = 0; j != h->n_buckets; ++j) { \ - if (__ac_iseither(h->flags, j) == 0) { \ - khkey_t key = h->keys[j]; \ - khval_t val; \ - khint_t new_mask; \ - new_mask = new_n_buckets - 1; \ - if (kh_is_map) val = h->vals[j]; \ - __ac_set_isdel_true(h->flags, j); \ - while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ - khint_t k, i, step = 0; \ - k = __hash_func(key); \ - i = k & new_mask; \ - while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ - __ac_set_isempty_false(new_flags, i); \ - if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ - { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ - if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ - __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ - } else { /* write the element and jump out of the loop */ \ - h->keys[i] = key; \ - if (kh_is_map) h->vals[i] = val; \ - break; \ - } \ - } \ - } \ - } \ - if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ - h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ - if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ - } \ - kfree(h->flags); /* free the working space */ \ - h->flags = new_flags; \ - h->n_buckets = new_n_buckets; \ - h->n_occupied = h->size; \ - h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ - } \ - } \ - SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ - REAL_FATTR_UNUSED; \ - SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ - { \ - khint_t x; \ - if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ - if (h->n_buckets > (h->size<<1)) { \ - kh_resize_##name(h, h->n_buckets - 1); /* clear "deleted" elements */ \ - } else { \ - kh_resize_##name(h, h->n_buckets + 1); /* expand the hash table */ \ - } \ - } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ - { \ - khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ - x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ - if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ - else { \ - last = i; \ - while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ - if (__ac_isdel(h->flags, i)) site = i; \ - i = (i + (++step)) & mask; \ - if (i == last) { x = site; break; } \ - } \ - if (x == h->n_buckets) { \ - if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ - else x = i; \ - } \ - } \ - } \ - if (__ac_isempty(h->flags, x)) { /* not present at all */ \ - h->keys[x] = key; \ - __ac_set_isboth_false(h->flags, x); \ - ++h->size; ++h->n_occupied; \ - *ret = 1; \ - } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ - h->keys[x] = key; \ - __ac_set_isboth_false(h->flags, x); \ - ++h->size; \ - *ret = 2; \ - } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ - return x; \ - } \ - SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ - REAL_FATTR_UNUSED; \ - SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ - { \ - if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ - __ac_set_isdel_true(h->flags, x); \ - --h->size; \ - } \ - } +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ + extern kh_##name##_t *kh_init_##name(void); \ + extern void kh_dealloc_##name(kh_##name##_t *h); \ + extern void kh_destroy_##name(kh_##name##_t *h); \ + extern void kh_clear_##name(kh_##name##_t *h); \ + extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ + extern void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ + extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ + extern void kh_del_##name(kh_##name##_t *h, khint_t x); + +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, \ + __hash_equal) \ + SCOPE kh_##name##_t *kh_init_##name(void) \ + REAL_FATTR_UNUSED; \ + SCOPE kh_##name##_t *kh_init_##name(void) { \ + return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ + } \ + SCOPE void kh_dealloc_##name(kh_##name##_t *h) \ + REAL_FATTR_UNUSED; \ + SCOPE void kh_dealloc_##name(kh_##name##_t *h) \ + { \ + kfree(h->keys); \ + kfree(h->flags); \ + kfree(h->vals); \ + } \ + SCOPE void kh_destroy_##name(kh_##name##_t *h) \ + REAL_FATTR_UNUSED; \ + SCOPE void kh_destroy_##name(kh_##name##_t *h) \ + { \ + if (h) { \ + kh_dealloc_##name(h); \ + kfree(h); \ + } \ + } \ + SCOPE void kh_clear_##name(kh_##name##_t *h) \ + REAL_FATTR_UNUSED; \ + SCOPE void kh_clear_##name(kh_##name##_t *h) \ + { \ + if (h && h->flags) { \ + memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ + h->size = h->n_occupied = 0; \ + } \ + } \ + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ + REAL_FATTR_UNUSED; \ + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ + { \ + if (h->n_buckets) { \ + khint_t k, i, last, mask, step = 0; \ + mask = h->n_buckets - 1; \ + k = __hash_func(key); i = k & mask; \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || \ + !__hash_equal(h->keys[i], key))) { \ + i = (i + (++step)) & mask; \ + if (i == last) { \ + return h->n_buckets; \ + } \ + } \ + return __ac_iseither(h->flags, i) ? h->n_buckets : i; \ + } else { \ + return 0; \ + } \ + } \ + SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ + REAL_FATTR_UNUSED; \ + SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ + { /* This function uses 0.25*n_buckets bytes of working space instead of */ \ + /* [sizeof(key_t+val_t)+.25]*n_buckets. */ \ + khint32_t *new_flags = 0; \ + khint_t j = 1; \ + { \ + kroundup32(new_n_buckets); \ + if (new_n_buckets < 4) { \ + new_n_buckets = 4; \ + } \ + /* requested size is too small */ \ + if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) { \ + j = 0; \ + } else { /* hash table size to be changed (shrink or expand); rehash */ \ + new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) \ + * sizeof(khint32_t)); \ + memset(new_flags, 0xaa, \ + __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (h->n_buckets < new_n_buckets) { /* expand */ \ + khkey_t *new_keys = (khkey_t*)krealloc( \ + (void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + h->keys = new_keys; \ + if (kh_is_map) { \ + khval_t *new_vals = (khval_t*)krealloc( \ + (void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + h->vals = new_vals; \ + } \ + } /* otherwise shrink */ \ + } \ + } \ + if (j) { /* rehashing is needed */ \ + for (j = 0; j != h->n_buckets; ++j) { \ + if (__ac_iseither(h->flags, j) == 0) { \ + khkey_t key = h->keys[j]; \ + khval_t val; \ + khint_t new_mask; \ + new_mask = new_n_buckets - 1; \ + if (kh_is_map) { \ + val = h->vals[j]; \ + } \ + __ac_set_isdel_true(h->flags, j); \ + /* kick-out process; sort of like in Cuckoo hashing */ \ + while (1) { \ + khint_t k, i, step = 0; \ + k = __hash_func(key); \ + i = k & new_mask; \ + while (!__ac_isempty(new_flags, i)) { \ + i = (i + (++step)) & new_mask; \ + } \ + __ac_set_isempty_false(new_flags, i); \ + /* kick out the existing element */ \ + if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { \ + { \ + khkey_t tmp = h->keys[i]; \ + h->keys[i] = key; \ + key = tmp; \ + } \ + if (kh_is_map) { \ + khval_t tmp = h->vals[i]; \ + h->vals[i] = val; \ + val = tmp; \ + } \ + /* mark it as deleted in the old hash table */ \ + __ac_set_isdel_true(h->flags, i); \ + } else { /* write the element and jump out of the loop */ \ + h->keys[i] = key; \ + if (kh_is_map) { \ + h->vals[i] = val; \ + } \ + break; \ + } \ + } \ + } \ + } \ + if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ + h->keys = (khkey_t*)krealloc((void *)h->keys, \ + new_n_buckets * sizeof(khkey_t)); \ + if (kh_is_map) { \ + h->vals = (khval_t*)krealloc((void *)h->vals, \ + new_n_buckets * sizeof(khval_t)); \ + } \ + } \ + kfree(h->flags); /* free the working space */ \ + h->flags = new_flags; \ + h->n_buckets = new_n_buckets; \ + h->n_occupied = h->size; \ + h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ + } \ + } \ + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ + REAL_FATTR_UNUSED; \ + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ + { \ + khint_t x; \ + if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ + if (h->n_buckets > (h->size << 1)) { \ + kh_resize_##name(h, h->n_buckets - 1); /* clear "deleted" elements */ \ + } else { \ + kh_resize_##name(h, h->n_buckets + 1); /* expand the hash table */ \ + } \ + } /* TODO: implement automatically shrinking; */ \ + /* resize() already support shrinking */ \ + { \ + khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ + x = site = h->n_buckets; \ + k = __hash_func(key); \ + i = k & mask; \ + if (__ac_isempty(h->flags, i)) { \ + x = i; /* for speed up */ \ + } else { \ + last = i; \ + while (!__ac_isempty(h->flags, i) \ + && (__ac_isdel(h->flags, i) \ + || !__hash_equal(h->keys[i], key))) { \ + if (__ac_isdel(h->flags, i)) { \ + site = i; \ + } \ + i = (i + (++step)) & mask; \ + if (i == last) { \ + x = site; \ + break; \ + } \ + } \ + if (x == h->n_buckets) { \ + if (__ac_isempty(h->flags, i) && site != h->n_buckets) { \ + x = site; \ + } else { \ + x = i; \ + } \ + } \ + } \ + } \ + if (__ac_isempty(h->flags, x)) { /* not present at all */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + h->size++; \ + h->n_occupied++; \ + *ret = 1; \ + } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + h->size++; \ + *ret = 2; \ + } else { \ + *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ + } \ + return x; \ + } \ + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ + REAL_FATTR_UNUSED; \ + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ + { \ + if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ + __ac_set_isdel_true(h->flags, x); \ + --h->size; \ + } \ + } #define KHASH_DECLARE(name, khkey_t, khval_t) \ __KHASH_TYPE(name, khkey_t, khval_t) \ @@ -447,6 +510,13 @@ static kh_inline khint_t __ac_Wang_hash(khint_t key) #define kh_destroy(name, h) kh_destroy_##name(h) /*! @function + @abstract Free memory referenced directly inside a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_dealloc(name, h) kh_dealloc_##name(h) + +/*! @function @abstract Reset a hash table without deallocating memory. @param name Name of the hash table [symbol] @param h Pointer to the hash table [khash_t(name)*] @@ -577,6 +647,24 @@ static kh_inline khint_t __ac_Wang_hash(khint_t key) code; \ } } +/*! @function + @abstract Iterate over the keys in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param kvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach_key(h, kvar, code) \ + { \ + khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); __i++) { \ + if (!kh_exist(h, __i)) { \ + continue; \ + } \ + (kvar) = kh_key(h, __i); \ + code; \ + } \ + } + /* More conenient interfaces */ /*! @function @@ -622,6 +710,21 @@ typedef const char *kh_cstr_t; @param name Name of the hash table [symbol] @param khval_t Type of values [type] */ -#define KHASH_MAP_INIT_STR(name, khval_t) \ - KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) -#endif /* __AC_KHASH_H */ +#define KHASH_MAP_INIT_STR(name, khval_t) \ + KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) + +/*! @function + @abstract Return a literal for an empty hash table. + @param name Name of the hash table [symbol] + */ +#define KHASH_EMPTY_TABLE(name) \ + ((kh_##name##_t) { \ + .n_buckets = 0, \ + .size = 0, \ + .n_occupied = 0, \ + .upper_bound = 0, \ + .flags = NULL, \ + .keys = NULL, \ + .vals = NULL, \ + }) +#endif // NVIM_LIB_KHASH_H diff --git a/src/nvim/lib/ringbuf.h b/src/nvim/lib/ringbuf.h new file mode 100644 index 0000000000..cb71500bb7 --- /dev/null +++ b/src/nvim/lib/ringbuf.h @@ -0,0 +1,281 @@ +/// Macros-based ring buffer implementation. +/// +/// Supported functions: +/// +/// - new: allocates new ring buffer. +/// - dealloc: free ring buffer itself. +/// - free: free ring buffer and all its elements. +/// - push: adds element to the end of the buffer. +/// - length: get buffer length. +/// - size: size of the ring buffer. +/// - idx: get element at given index. +/// - idx_p: get pointer to the element at given index. +/// - insert: insert element at given position. +/// - remove: remove element from given position. +#ifndef NVIM_LIB_RINGBUF_H +#define NVIM_LIB_RINGBUF_H + +#include <string.h> +#include <assert.h> +#include <stdint.h> + +#include "nvim/memory.h" +#include "nvim/func_attr.h" + +#define _RINGBUF_LENGTH(rb) \ + ((rb)->first == NULL ? 0 \ + : ((rb)->next == (rb)->first) ? (size_t) ((rb)->buf_end - (rb)->buf) + 1 \ + : ((rb)->next > (rb)->first) ? (size_t) ((rb)->next - (rb)->first) \ + : (size_t) ((rb)->next - (rb)->buf + (rb)->buf_end - (rb)->first + 1)) + +#define _RINGBUF_NEXT(rb, var) \ + ((var) == (rb)->buf_end ? (rb)->buf : (var) + 1) +#define _RINGBUF_PREV(rb, var) \ + ((var) == (rb)->buf ? (rb)->buf_end : (var) - 1) + +/// Iterate over all ringbuf values +/// +/// @param rb Ring buffer to iterate over. +/// @param RBType Type of the ring buffer element. +/// @param varname Variable name. +#define RINGBUF_FORALL(rb, RBType, varname) \ + size_t varname##_length_fa_ = _RINGBUF_LENGTH(rb); \ + for (RBType *varname = ((rb)->first == NULL ? (rb)->next : (rb)->first); \ + varname##_length_fa_; \ + (varname = _RINGBUF_NEXT(rb, varname)), \ + varname##_length_fa_--) + +/// Iterate over all ringbuf values, from end to the beginning +/// +/// Unlike previous RINGBUF_FORALL uses already defined variable, in place of +/// defining variable in the cycle body. +/// +/// @param rb Ring buffer to iterate over. +/// @param RBType Type of the ring buffer element. +/// @param varname Variable name. +#define RINGBUF_ITER_BACK(rb, RBType, varname) \ + size_t varname##_length_ib_ = _RINGBUF_LENGTH(rb); \ + for (varname = ((rb)->next == (rb)->buf ? (rb)->buf_end : (rb)->next - 1); \ + varname##_length_ib_; \ + (varname = _RINGBUF_PREV(rb, varname)), \ + varname##_length_ib_--) + +/// Define a ring buffer structure +/// +/// @param TypeName Ring buffer type name. Actual type name will be +/// `{TypeName}RingBuffer`. +/// @param RBType Type of the single ring buffer element. +#define RINGBUF_TYPEDEF(TypeName, RBType) \ +typedef struct { \ + RBType *buf; \ + RBType *next; \ + RBType *first; \ + RBType *buf_end; \ +} TypeName##RingBuffer; + +/// Initialize a new ring buffer +/// +/// @param TypeName Ring buffer type name. Actual type name will be +/// `{TypeName}RingBuffer`. +/// @param funcprefix Prefix for all ring buffer functions. Function name will +/// look like `{funcprefix}_rb_{function_name}`. +/// @param RBType Type of the single ring buffer element. +/// @param rbfree Function used to free ring buffer element. May be +/// a macros like `#define RBFREE(item)` (to skip freeing). +/// +/// Intended function signature: `void *rbfree(RBType *)`; +#define RINGBUF_INIT(TypeName, funcprefix, RBType, rbfree) \ + \ + \ +static inline TypeName##RingBuffer funcprefix##_rb_new(const size_t size) \ + REAL_FATTR_WARN_UNUSED_RESULT; \ +static inline TypeName##RingBuffer funcprefix##_rb_new(const size_t size) \ +{ \ + assert(size != 0); \ + RBType *buf = xmalloc(size * sizeof(RBType)); \ + return (TypeName##RingBuffer) { \ + .buf = buf, \ + .next = buf, \ + .first = NULL, \ + .buf_end = buf + size - 1, \ + }; \ +} \ + \ +static inline void funcprefix##_rb_free(TypeName##RingBuffer *const rb) \ + REAL_FATTR_UNUSED; \ +static inline void funcprefix##_rb_free(TypeName##RingBuffer *const rb) \ +{ \ + if (rb == NULL) { \ + return; \ + } \ + RINGBUF_FORALL(rb, RBType, rbitem) { \ + rbfree(rbitem); \ + } \ + xfree(rb->buf); \ +} \ + \ +static inline void funcprefix##_rb_dealloc(TypeName##RingBuffer *const rb) \ + REAL_FATTR_UNUSED; \ +static inline void funcprefix##_rb_dealloc(TypeName##RingBuffer *const rb) \ +{ \ + xfree(rb->buf); \ +} \ + \ +static inline void funcprefix##_rb_push(TypeName##RingBuffer *const rb, \ + RBType item) \ + REAL_FATTR_NONNULL_ARG(1); \ +static inline void funcprefix##_rb_push(TypeName##RingBuffer *const rb, \ + RBType item) \ +{ \ + if (rb->next == rb->first) { \ + rbfree(rb->first); \ + rb->first = _RINGBUF_NEXT(rb, rb->first); \ + } else if (rb->first == NULL) { \ + rb->first = rb->next; \ + } \ + *rb->next = item; \ + rb->next = _RINGBUF_NEXT(rb, rb->next); \ +} \ + \ +static inline ptrdiff_t funcprefix##_rb_find_idx( \ + const TypeName##RingBuffer *const rb, const RBType *const item_p) \ + REAL_FATTR_NONNULL_ALL REAL_FATTR_PURE REAL_FATTR_UNUSED; \ +static inline ptrdiff_t funcprefix##_rb_find_idx( \ + const TypeName##RingBuffer *const rb, const RBType *const item_p) \ +{ \ + assert(rb->buf <= item_p); \ + assert(rb->buf_end >= item_p); \ + if (rb->first == NULL) { \ + return -1; \ + } else if (item_p >= rb->first) { \ + return item_p - rb->first; \ + } else { \ + return item_p - rb->buf + rb->buf_end - rb->first + 1; \ + } \ +} \ + \ +static inline size_t funcprefix##_rb_size( \ + const TypeName##RingBuffer *const rb) \ + REAL_FATTR_NONNULL_ALL REAL_FATTR_PURE; \ +static inline size_t funcprefix##_rb_size( \ + const TypeName##RingBuffer *const rb) \ +{ \ + return (size_t) (rb->buf_end - rb->buf) + 1; \ +} \ + \ +static inline size_t funcprefix##_rb_length( \ + const TypeName##RingBuffer *const rb) \ + REAL_FATTR_NONNULL_ALL REAL_FATTR_PURE; \ +static inline size_t funcprefix##_rb_length( \ + const TypeName##RingBuffer *const rb) \ +{ \ + return _RINGBUF_LENGTH(rb); \ +} \ + \ +static inline RBType *funcprefix##_rb_idx_p( \ + const TypeName##RingBuffer *const rb, const size_t idx) \ + REAL_FATTR_NONNULL_ALL REAL_FATTR_PURE; \ +static inline RBType *funcprefix##_rb_idx_p( \ + const TypeName##RingBuffer *const rb, const size_t idx) \ +{ \ + assert(idx <= funcprefix##_rb_size(rb)); \ + assert(idx <= funcprefix##_rb_length(rb)); \ + if (rb->first + idx > rb->buf_end) { \ + return rb->buf + ((rb->first + idx) - (rb->buf_end + 1)); \ + } else { \ + return rb->first + idx; \ + } \ +} \ + \ +static inline RBType funcprefix##_rb_idx(const TypeName##RingBuffer *const rb, \ + const size_t idx) \ + REAL_FATTR_NONNULL_ALL REAL_FATTR_PURE REAL_FATTR_UNUSED; \ +static inline RBType funcprefix##_rb_idx(const TypeName##RingBuffer *const rb, \ + const size_t idx) \ +{ \ + return *funcprefix##_rb_idx_p(rb, idx); \ +} \ + \ +static inline void funcprefix##_rb_insert(TypeName##RingBuffer *const rb, \ + const size_t idx, \ + RBType item) \ + REAL_FATTR_NONNULL_ARG(1) REAL_FATTR_UNUSED; \ +static inline void funcprefix##_rb_insert(TypeName##RingBuffer *const rb, \ + const size_t idx, \ + RBType item) \ +{ \ + assert(idx <= funcprefix##_rb_size(rb)); \ + assert(idx <= funcprefix##_rb_length(rb)); \ + const size_t length = funcprefix##_rb_length(rb); \ + if (idx == length) { \ + funcprefix##_rb_push(rb, item); \ + return; \ + } \ + RBType *const insertpos = funcprefix##_rb_idx_p(rb, idx); \ + if (insertpos == rb->next) { \ + funcprefix##_rb_push(rb, item); \ + return; \ + } \ + if (length == funcprefix##_rb_size(rb)) { \ + rbfree(rb->first); \ + } \ + if (insertpos < rb->next) { \ + memmove(insertpos + 1, insertpos, \ + (size_t) ((uintptr_t) rb->next - (uintptr_t) insertpos)); \ + } else { \ + assert(insertpos > rb->first); \ + assert(rb->next <= rb->first); \ + memmove(rb->buf + 1, rb->buf, \ + (size_t) ((uintptr_t) rb->next - (uintptr_t) rb->buf)); \ + *rb->buf = *rb->buf_end; \ + memmove(insertpos + 1, insertpos, \ + (size_t) ((uintptr_t) (rb->buf_end + 1) - (uintptr_t) insertpos)); \ + } \ + *insertpos = item; \ + if (length == funcprefix##_rb_size(rb)) { \ + rb->first = _RINGBUF_NEXT(rb, rb->first); \ + } \ + rb->next = _RINGBUF_NEXT(rb, rb->next); \ +} \ + \ +static inline void funcprefix##_rb_remove(TypeName##RingBuffer *const rb, \ + const size_t idx) \ + REAL_FATTR_NONNULL_ARG(1) REAL_FATTR_UNUSED; \ +static inline void funcprefix##_rb_remove(TypeName##RingBuffer *const rb, \ + const size_t idx) \ +{ \ + assert(idx < funcprefix##_rb_size(rb)); \ + assert(idx < funcprefix##_rb_length(rb)); \ + RBType *const rmpos = funcprefix##_rb_idx_p(rb, idx); \ + rbfree(rmpos); \ + if (rmpos == rb->next - 1) { \ + rb->next--; \ + if (rb->first == rb->next) { \ + rb->first = NULL; \ + rb->next = rb->buf; \ + } \ + } else if (rmpos == rb->first) { \ + rb->first = _RINGBUF_NEXT(rb, rb->first); \ + if (rb->first == rb->next) { \ + rb->first = NULL; \ + rb->next = rb->buf; \ + } \ + } else if (rb->first < rb->next || rb->next == rb->buf) { \ + assert(rmpos > rb->first); \ + assert(rmpos <= _RINGBUF_PREV(rb, rb->next)); \ + memmove(rb->first + 1, rb->first, \ + (size_t) ((uintptr_t) rmpos - (uintptr_t) rb->first)); \ + rb->first = _RINGBUF_NEXT(rb, rb->first); \ + } else if (rmpos < rb->next) { \ + memmove(rmpos, rmpos + 1, \ + (size_t) ((uintptr_t) rb->next - (uintptr_t) rmpos)); \ + rb->next = _RINGBUF_PREV(rb, rb->next); \ + } else { \ + assert(rb->first < rb->buf_end); \ + memmove(rb->first + 1, rb->first, \ + (size_t) ((uintptr_t) rmpos - (uintptr_t) rb->first)); \ + rb->first = _RINGBUF_NEXT(rb, rb->first); \ + } \ +} + +#endif // NVIM_LIB_RINGBUF_H diff --git a/src/nvim/main.c b/src/nvim/main.c index 27f8340ec7..d865260295 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, 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"); + if (*p_shada != NUL) { + shada_read_everything(NULL, false, true); + 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_shada && *p_shada != NUL) { + // Write out the registers, history, marks etc, to the ShaDa 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")); // NOLINT(whitespace/line_length) 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 ce44149ffa..dd49b311d3 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -39,6 +39,7 @@ #include "nvim/strings.h" #include "nvim/ui.h" #include "nvim/os/os.h" +#include "nvim/os/time.h" #include "nvim/os/input.h" /* @@ -48,13 +49,13 @@ /* * 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]; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "mark.c.generated.h" @@ -68,6 +69,27 @@ int setmark(int c) return setmark_pos(c, &curwin->w_cursor, curbuf->b_fnum); } +/// Free fmark_T item +void free_fmark(fmark_T fm) +{ + dict_unref(fm.additional_data); +} + +/// Free xfmark_T item +void free_xfmark(xfmark_T fm) +{ + xfree(fm.fname); + free_fmark(fm.fmark); +} + +/// Free and clear fmark_T item +void clear_fmark(fmark_T *fm) + FUNC_ATTR_NONNULL_ALL +{ + free_fmark(*fm); + memset(fm, 0, sizeof(*fm)); +} + /* * Set named mark "c" to position "pos". * When "c" is upper case use file "fnum". @@ -92,7 +114,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 +145,13 @@ 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; + RESET_XFMARK(namedfm + i, *pos, fnum, NULL); return OK; } return FAIL; @@ -144,7 +163,6 @@ int setmark_pos(int c, pos_T *pos, int fnum) */ void setpcmark(void) { - int i; xfmark_T *fm; /* for :global the mark is set only once */ @@ -157,16 +175,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); - for (i = 1; i < JUMPLISTSIZE; ++i) - curwin->w_jumplist[i - 1] = curwin->w_jumplist[i]; + free_xfmark(curwin->w_jumplist[0]); + memmove(&curwin->w_jumplist[0], &curwin->w_jumplist[1], + (JUMPLISTSIZE - 1) * sizeof(curwin->w_jumplist[0])); } 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); } /* @@ -260,7 +276,7 @@ pos_T *movechangelist(int count) } else n += count; curwin->w_changelistidx = n; - return curbuf->b_changelist + n; + return &(curbuf->b_changelist[n].mark); } /* @@ -296,22 +312,21 @@ pos_T *getmark_buf_fnum(buf_T *buf, int c, int changefile, int *fnum) * to crash. */ if (c < 0) return posp; - if (c > '~') /* check for islower()/isupper() */ - ; - else if (c == '\'' || c == '`') { /* previous context mark */ - 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); - else if (c == '^') /* to where Insert mode stopped */ - posp = &(buf->b_last_insert); - else if (c == '.') /* to where last change was made */ - posp = &(buf->b_last_change); - else if (c == '[') /* to start of previous operator */ + if (c > '~') { // check for islower()/isupper() + } else if (c == '\'' || c == '`') { // previous context mark + 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.mark); + } else if (c == '^') { // to where Insert mode stopped + posp = &(buf->b_last_insert.mark); + } else if (c == '.') { // to where last change was made + 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 */ + } else if (c == ']') { // to end of previous operator posp = &(buf->b_op_end); - else if (c == '{' || c == '}') { /* to previous/next paragraph */ + } else if (c == '{' || c == '}') { // to previous/next paragraph pos_T pos; oparg_T oa; int slcb = listcmd_busy; @@ -357,7 +372,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 +380,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 +436,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 +454,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 +491,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 +509,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 +553,12 @@ int check_mark(pos_T *pos) */ void clrallmarks(buf_T *buf) { - static int i = -1; - - if (i == -1) /* first call ever: initialize */ - for (i = 0; i < NMARKS + 1; i++) { - namedfm[i].fmark.mark.lnum = 0; - namedfm[i].fname = NULL; - } - - 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; } @@ -610,10 +611,10 @@ void do_marks(exarg_T *eap) if (arg != NULL && *arg == NUL) arg = NULL; - show_one_mark('\'', arg, &curwin->w_pcmark, NULL, TRUE); + 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,14 +627,14 @@ void do_marks(exarg_T *eap) xfree(name); } } - show_one_mark('"', arg, &curbuf->b_last_cursor, 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_visual.vi_start, NULL, TRUE); - show_one_mark('>', arg, &curbuf->b_visual.vi_end, NULL, TRUE); - show_one_mark(-1, arg, NULL, NULL, FALSE); + 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.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); } static void @@ -727,13 +728,14 @@ void ex_delmarks(exarg_T *eap) from = to = *p; for (i = from; i <= to; ++i) { - if (lower) - curbuf->b_namedm[i - 'a'].lnum = 0; - else { - if (digit) + if (lower) { + curbuf->b_namedm[i - 'a'].mark.lnum = 0; + } else { + if (digit) { n = i - '0' + NMARKS; - else + } else { n = i - 'A'; + } namedfm[n].fmark.mark.lnum = 0; xfree(namedfm[n].fname); namedfm[n].fname = NULL; @@ -741,9 +743,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; @@ -811,7 +813,7 @@ void ex_changes(exarg_T *eap) MSG_PUTS_TITLE(_("\nchange line col text")); for (i = 0; i < curbuf->b_changelistlen && !got_int; ++i) { - if (curbuf->b_changelist[i].lnum != 0) { + if (curbuf->b_changelist[i].mark.lnum != 0) { msg_putchar('\n'); if (got_int) break; @@ -819,10 +821,10 @@ void ex_changes(exarg_T *eap) i == curwin->w_changelistidx ? '>' : ' ', i > curwin->w_changelistidx ? i - curwin->w_changelistidx : curwin->w_changelistidx - i, - (long)curbuf->b_changelist[i].lnum, - curbuf->b_changelist[i].col); + (long)curbuf->b_changelist[i].mark.lnum, + curbuf->b_changelist[i].mark.col); msg_outtrans(IObuff); - name = mark_line(&curbuf->b_changelist[i], 17); + name = mark_line(&curbuf->b_changelist[i].mark, 17); msg_outtrans_attr(name, hl_attr(HLF_D)); xfree(name); os_breakcheck(); @@ -886,29 +888,29 @@ 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 */ for (i = 0; i < curbuf->b_changelistlen; ++i) - one_adjust_nodel(&(curbuf->b_changelist[i].lnum)); + one_adjust_nodel(&(curbuf->b_changelist[i].mark.lnum)); /* Visual area */ one_adjust_nodel(&(curbuf->b_visual.vi_start.lnum)); @@ -1038,24 +1040,24 @@ 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) - col_adjust(&(curbuf->b_changelist[i])); + col_adjust(&(curbuf->b_changelist[i].mark)); /* Visual area */ col_adjust(&(curbuf->b_visual.vi_start)); @@ -1101,7 +1103,7 @@ void mark_col_adjust(linenr_T lnum, colnr_T mincol, long lnum_amount, long col_a * When deleting lines, this may create duplicate marks in the * jumplist. They will be removed here for the current window. */ -static void cleanup_jumplist(void) +void cleanup_jumplist(void) { int i; int from, to; @@ -1117,10 +1119,17 @@ static void cleanup_jumplist(void) && curwin->w_jumplist[i].fmark.mark.lnum == curwin->w_jumplist[from].fmark.mark.lnum) break; - if (i >= curwin->w_jumplistlen) /* no duplicate */ - curwin->w_jumplist[to++] = curwin->w_jumplist[from]; - else + if (i >= curwin->w_jumplistlen) { // no duplicate + if (to != from) { + // Not using curwin->w_jumplist[to++] = curwin->w_jumplist[from] because + // this way valgrind complains about overlapping source and destination + // in memcpy() call. (clang-3.6.0, debug build with -DEXITFREE). + curwin->w_jumplist[to] = curwin->w_jumplist[from]; + } + to++; + } else { xfree(curwin->w_jumplist[from].fname); + } } if (curwin->w_jumplistidx == curwin->w_jumplistlen) curwin->w_jumplistidx = to; @@ -1143,382 +1152,253 @@ 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_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[0]) + : (const xfmark_T *const) iter); + *fm = *iter_mark; + if (iter_mark == &(win->w_jumplist[win->w_jumplistlen - 1])) { + 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_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); + *name = NUL; + const xfmark_T *iter_mark = (iter == NULL + ? &(namedfm[0]) + : (const xfmark_T *const) iter); + while ((size_t) (iter_mark - &(namedfm[0])) < ARRAY_SIZE(namedfm) + && !iter_mark->fmark.mark.lnum) { + iter_mark++; + } + if ((size_t) (iter_mark - &(namedfm[0])) == ARRAY_SIZE(namedfm) + || !iter_mark->fmark.mark.lnum) { + return NULL; + } + 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 FUNC_ATTR_WARN_UNUSED_RESULT { - 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); } - 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 = '^'; + return &(buf->b_last_insert); + } + case '^': { + *mark_name = '.'; + return &(buf->b_last_change); + } + 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_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; + *name = NUL; + 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) { + 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) +/// 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. +/// +/// @return true on success, false on failure. +bool mark_set_global(const char name, const xfmark_T fm, const bool update) { - 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); + const int idx = mark_global_index(name); + if (idx == -1) { + return false; } - - if (fm->fmark.fnum != 0) - xfree(name); + xfmark_T *const fm_tgt = &(namedfm[idx]); + if (update && fm.fmark.timestamp <= fm_tgt->fmark.timestamp) { + return false; + } + if (fm_tgt->fmark.mark.lnum != 0) { + free_xfmark(*fm_tgt); + } + *fm_tgt = fm; + return true; } -/* - * Return TRUE if "name" is on removable media (depending on 'viminfo'). - */ -int removable(char_u *name) +/// 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. +/// +/// @return true on success, false on failure. +bool mark_set_local(const char name, buf_T *const buf, + const fmark_T fm, const bool update) + 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; - } - } + 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 false; + } + if (update && fm.timestamp <= fm_tgt->timestamp) { + return false; } - xfree(name); - return retval; + if (fm_tgt->mark.lnum != 0) { + free_fmark(*fm_tgt); + } + *fm_tgt = fm; + return true; } - /* - * Write all the named marks for all buffers. - * Return the number of buffers for which marks have been written. + * Free items in the jumplist of window "wp". */ -int write_viminfo_marks(FILE *fp_out) +void free_jumplist(win_T *wp) { - /* - * Set b_last_cursor for the all buffers that have a window. - */ - FOR_ALL_TAB_WINDOWS(tp, win) { - set_last_cursor(win); - } + int i; - 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++; - } - } + for (i = 0; i < wp->w_jumplistlen; ++i) { + free_xfmark(wp->w_jumplist[i]); } - - return count; + wp->w_jumplistlen = 0; } -static void write_one_mark(FILE *fp_out, int c, pos_T *pos) +void set_last_cursor(win_T *win) { - if (pos->lnum != 0) - fprintf(fp_out, "\t%c\t%" PRId64 "\t%d\n", c, - (int64_t)pos->lnum, (int)pos->col); + if (win->w_buffer != NULL) { + RESET_FMARK(&win->w_buffer->b_last_cursor, win->w_cursor, 0); + } } -/* - * 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) +#if defined(EXITFREE) +void free_all_marks(void) { - 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++; - } - } - 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); + memset(&namedfm[0], 0, sizeof(namedfm)); } +#endif diff --git a/src/nvim/mark.h b/src/nvim/mark.h index aa89a5b625..aff6e7273a 100644 --- a/src/nvim/mark.h +++ b/src/nvim/mark.h @@ -1,9 +1,78 @@ #ifndef NVIM_MARK_H #define NVIM_MARK_H +#include "nvim/macros.h" +#include "nvim/ascii.h" #include "nvim/buffer_defs.h" #include "nvim/mark_defs.h" +#include "nvim/memory.h" #include "nvim/pos.h" +#include "nvim/os/time.h" + +/// Set fmark using given value +#define SET_FMARK(fmarkp_, mark_, fnum_) \ + do { \ + fmark_T *const fmarkp__ = fmarkp_; \ + fmarkp__->mark = mark_; \ + fmarkp__->fnum = fnum_; \ + fmarkp__->timestamp = os_time(); \ + fmarkp__->additional_data = NULL; \ + } while (0) + +/// Free and set fmark using given value +#define RESET_FMARK(fmarkp_, mark_, fnum_) \ + do { \ + fmark_T *const fmarkp___ = fmarkp_; \ + free_fmark(*fmarkp___); \ + SET_FMARK(fmarkp___, mark_, fnum_); \ + } while (0) + +/// Clear given fmark +#define CLEAR_FMARK(fmarkp_) \ + RESET_FMARK(fmarkp_, ((pos_T) {0, 0, 0}), 0) + +/// Set given extended mark (regular mark + file name) +#define SET_XFMARK(xfmarkp_, mark_, fnum_, fname_) \ + do { \ + xfmark_T *const xfmarkp__ = xfmarkp_; \ + xfmarkp__->fname = fname_; \ + SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_); \ + } while (0) + +/// Free and set given extended mark (regular mark + file name) +#define RESET_XFMARK(xfmarkp_, mark_, fnum_, fname_) \ + do { \ + xfmark_T *const xfmarkp__ = xfmarkp_; \ + free_xfmark(*xfmarkp__); \ + xfmarkp__->fname = fname_; \ + SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_); \ + } while (0) + +/// Convert mark name to the offset +static inline int mark_global_index(const char name) + FUNC_ATTR_CONST +{ + return (ASCII_ISUPPER(name) + ? (name - 'A') + : (ascii_isdigit(name) + ? (NMARKS + (name - '0')) + : -1)); +} + +/// Convert local mark name to the offset +static inline int mark_local_index(const char name) + FUNC_ATTR_CONST +{ + return (ASCII_ISLOWER(name) + ? (name - 'a') + : (name == '"' + ? NMARKS + : (name == '^' + ? NMARKS + 1 + : (name == '.' + ? NMARKS + 2 + : -1)))); +} #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..720b2475ed 100644 --- a/src/nvim/mark_defs.h +++ b/src/nvim/mark_defs.h @@ -2,25 +2,47 @@ #define NVIM_MARK_DEFS_H #include "nvim/pos.h" +#include "nvim/os/time.h" +#include "nvim/eval_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) + +/// Total possible number of local marks +/// +/// That are uppercase marks plus '"', '^' and '.'. There are other local marks, +/// but they are not saved in ShaDa files. +#define NLOCALMARKS (NMARKS + 3) + +/// 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. + dict_T *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/memory.c b/src/nvim/memory.c index 132c895997..d25dc7c941 100644 --- a/src/nvim/memory.c +++ b/src/nvim/memory.c @@ -30,7 +30,7 @@ /// Try to free memory. Used when trying to recover from out of memory errors. /// @see {xmalloc} -static void try_to_free_memory(void) +void try_to_free_memory(void) { static bool trying_to_free = false; // avoid recursive calls diff --git a/src/nvim/misc1.c b/src/nvim/misc1.c index 8407198b13..1f1b5c2aa9 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. */ @@ -2054,7 +2053,7 @@ static void changed_common(linenr_T lnum, colnr_T col, linenr_T lnume, long xtra /* Don't create a new entry when the line number is the same * as the last one and the column is not too far away. Avoids * creating many entries for typing "xxxxx". */ - p = &curbuf->b_changelist[curbuf->b_changelistlen - 1]; + p = &curbuf->b_changelist[curbuf->b_changelistlen - 1].mark; if (p->lnum != lnum) add = TRUE; else { @@ -2074,7 +2073,7 @@ static void changed_common(linenr_T lnum, colnr_T col, linenr_T lnume, long xtra /* changelist is full: remove oldest entry */ curbuf->b_changelistlen = JUMPLISTSIZE - 1; memmove(curbuf->b_changelist, curbuf->b_changelist + 1, - sizeof(pos_T) * (JUMPLISTSIZE - 1)); + sizeof(curbuf->b_changelist[0]) * (JUMPLISTSIZE - 1)); FOR_ALL_TAB_WINDOWS(tp, wp) { /* Correct position in changelist for other windows on * this buffer. */ 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..3fd2c0b773 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -50,26 +50,11 @@ #include "nvim/undo.h" #include "nvim/window.h" #include "nvim/os/input.h" - -/* - * Registers: - * 0 = register for latest (unnamed) yank - * 1..9 = registers '1' to '9', for deletes - * 10..35 = registers 'a' to 'z' - * 36 = delete register '-' - * 37 = selection register '*' - * 38 = clipboard register '+' - */ -#define DELETION_REGISTER 36 -#define NUM_SAVED_REGISTERS 37 -// The following registers should not be saved in viminfo: -#define STAR_REGISTER 37 -#define PLUS_REGISTER 38 -#define NUM_REGISTERS 39 +#include "nvim/os/time.h" 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; @@ -778,19 +763,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 = op_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 +867,16 @@ int do_record(int c) return retval; } +static void set_yreg_additional_data(yankreg_T *reg, dict_T *additional_data) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (reg->additional_data == additional_data) { + return; + } + dict_unref(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 +906,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 +2255,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 +2277,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 +2356,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 *)); + reg->additional_data = NULL; + reg->timestamp = os_time(); y_idx = 0; lnum = oap->start.lnum; @@ -4433,171 +4422,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 +4563,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 +4796,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 +5188,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 ShaDa file. int i = 0; for (listitem_T *li = lines->lv_first; li != NULL; li = li->li_next) { @@ -5411,6 +5240,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 +5309,91 @@ 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_PURE +{ + 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_NONNULL_ARG(2, 3) FUNC_ATTR_WARN_UNUSED_RESULT +{ + *name = NUL; + const yankreg_T *iter_reg = (iter == NULL + ? &(y_regs[0]) + : (const yankreg_T *const) iter); + while (iter_reg - &(y_regs[0]) < NUM_SAVED_REGISTERS && reg_empty(iter_reg)) { + iter_reg++; + } + if (iter_reg - &(y_regs[0]) == NUM_SAVED_REGISTERS || reg_empty(iter_reg)) { + 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_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 +/// +/// @param[in] name Register name. +/// @param[in] reg Register value. +/// +/// @return true on success, false on failure. +bool op_register_set(const char name, const yankreg_T reg) +{ + int i = op_reg_index(name); + if (i == -1) { + return false; + } + free_register(&y_regs[i]); + y_regs[i] = reg; + return true; +} + +/// Get register with the given name +/// +/// @param[in] name Register name. +/// +/// @return Pointer to the register contents or NULL. +const yankreg_T *op_register_get(const char name) +{ + int i = op_reg_index(name); + if (i == -1) { + return NULL; + } + return &y_regs[i]; +} diff --git a/src/nvim/ops.h b/src/nvim/ops.h index 99683165f9..507f933acf 100644 --- a/src/nvim/ops.h +++ b/src/nvim/ops.h @@ -3,7 +3,11 @@ #include <stdbool.h> +#include "nvim/macros.h" +#include "nvim/ascii.h" #include "nvim/types.h" +#include "nvim/eval_defs.h" +#include "nvim/os/time.h" typedef int (*Indenter)(void); @@ -16,6 +20,22 @@ typedef int (*Indenter)(void); #define PUT_LINE_FORWARD 32 /* put linewise register below Visual sel. */ /* + * Registers: + * 0 = register for latest (unnamed) yank + * 1..9 = registers '1' to '9', for deletes + * 10..35 = registers 'a' to 'z' + * 36 = delete register '-' + * 37 = selection register '*' + * 38 = clipboard register '+' + */ +#define DELETION_REGISTER 36 +#define NUM_SAVED_REGISTERS 37 +// The following registers should not be saved in ShaDa file: +#define STAR_REGISTER 37 +#define PLUS_REGISTER 38 +#define NUM_REGISTERS 39 + +/* * Operator IDs; The order must correspond to opchars[] in ops.c! */ #define OP_NOP 0 /* no pending operation */ @@ -47,14 +67,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 +74,41 @@ 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. + dict_T *additional_data; ///< Additional data from ShaDa file. +} yankreg_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 op_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; + } +} + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ops.h.generated.h" #endif diff --git a/src/nvim/option.c b/src/nvim/option.c index c47616620c..cbb22a0546 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -1738,16 +1738,16 @@ set_options_bin ( /* * Find the parameter represented by the given character (eg ', :, ", or /), - * and return its associated value in the 'viminfo' string. + * and return its associated value in the 'shada' string. * Only works for number parameters, not for 'r' or 'n'. * If the parameter is not specified in the string or there is no following * number, return -1. */ -int get_viminfo_parameter(int type) +int get_shada_parameter(int type) { char_u *p; - p = find_viminfo_parameter(type); + p = find_shada_parameter(type); if (p != NULL && ascii_isdigit(*p)) return atoi((char *)p); return -1; @@ -1755,14 +1755,14 @@ int get_viminfo_parameter(int type) /* * Find the parameter represented by the given character (eg ''', ':', '"', or - * '/') in the 'viminfo' option and return a pointer to the string after it. + * '/') in the 'shada' option and return a pointer to the string after it. * Return NULL if the parameter is not specified in the string. */ -char_u *find_viminfo_parameter(int type) +char_u *find_shada_parameter(int type) { char_u *p; - for (p = p_viminfo; *p; ++p) { + for (p = p_shada; *p; ++p) { if (*p == type) return p + 1; if (*p == 'n') /* 'n' is always the last one */ @@ -1968,6 +1968,8 @@ static void redraw_titles(void) { redraw_tabline = TRUE; } +static int shada_idx = -1; + /* * Set a string option to a new value (without checking the effect). * The string is copied into allocated memory. @@ -2001,6 +2003,8 @@ set_string_option_direct ( if (options[idx].var == NULL) /* can't set hidden option */ return; + assert((void *) options[idx].var != (void *) &p_shada); + s = vim_strsave(val); { varp = (char_u **)get_varp_scope(&(options[idx]), @@ -2441,10 +2445,16 @@ did_set_string_option ( verbose_stop(); if (*p_vfile != NUL && verbose_open() == FAIL) errmsg = e_invarg; - } - /* 'viminfo' */ - else if (varp == &p_viminfo) { - for (s = p_viminfo; *s; ) { + /* 'shada' */ + } else if (varp == &p_shada) { + // TODO(ZyX-I): Remove this code in the future, alongside with &viminfo + // option. + opt_idx = ((options[opt_idx].fullname[0] == 'v') + ? (shada_idx == -1 + ? ((shada_idx = findoption((char_u *) "shada"))) + : shada_idx) + : opt_idx); + for (s = p_shada; *s; ) { /* Check it's a valid character */ if (vim_strchr((char_u *)"!\"%'/:<@cfhnrs", *s) == NULL) { errmsg = illegal_char(errbuf, *s); @@ -2486,7 +2496,7 @@ did_set_string_option ( break; } } - if (*p_viminfo && errmsg == NULL && get_viminfo_parameter('\'') < 0) + if (*p_shada && errmsg == NULL && get_shada_parameter('\'') < 0) errmsg = (char_u *)N_("E528: Must specify a ' value"); } /* 'showbreak' */ diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 5340f4bdd5..ab3169bff6 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -558,7 +558,7 @@ EXTERN long p_ur; /* 'undoreload' */ EXTERN long p_uc; /* 'updatecount' */ EXTERN long p_ut; /* 'updatetime' */ EXTERN char_u *p_fcs; /* 'fillchar' */ -EXTERN char_u *p_viminfo; /* 'viminfo' */ +EXTERN char_u *p_shada; /* 'shada' */ EXTERN char_u *p_vdir; /* 'viewdir' */ EXTERN char_u *p_vop; /* 'viewoptions' */ EXTERN unsigned vop_flags; /* uses SSOP_ flags */ diff --git a/src/nvim/options.lua b/src/nvim/options.lua index ca3882b8a0..5973e4b938 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1992,6 +1992,14 @@ return { }} }, { + full_name='shada', abbreviation='sd', + type='string', list='comma', scope={'global'}, + deny_duplicates=true, + secure=true, + varname='p_shada', + defaults={if_true={vi="", vim="!,'100,<50,s10,h"}} + }, + { full_name='shell', abbreviation='sh', type='string', scope={'global'}, secure=true, @@ -2584,7 +2592,7 @@ return { type='string', list='comma', scope={'global'}, deny_duplicates=true, secure=true, - varname='p_viminfo', + varname='p_shada', defaults={if_true={vi="", vim="!,'100,<50,s10,h"}} }, { diff --git a/src/nvim/os/fs_defs.h b/src/nvim/os/fs_defs.h index ddd382a3cb..df1031b721 100644 --- a/src/nvim/os/fs_defs.h +++ b/src/nvim/os/fs_defs.h @@ -21,4 +21,9 @@ typedef struct { uv_dirent_t ent; ///< @private The entry information. } Directory; +/// Function to convert -errno error to char * error description +/// +/// -errno errors are returned by a number of os functions. +#define os_strerror uv_strerror + #endif // NVIM_OS_FS_DEFS_H 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..ad4886446a 100644 --- a/src/nvim/os/time.h +++ b/src/nvim/os/time.h @@ -5,6 +5,8 @@ #include <stdbool.h> #include <time.h> +typedef uint64_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..8ba888841c 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -79,23 +79,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 +86,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 +241,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 +278,29 @@ 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); + dict_unref(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]); + + memset(spats, 0, sizeof(spats)); if (mr_pattern_alloced) { xfree(mr_pattern); @@ -414,17 +409,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 +432,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 +1050,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 +1163,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 +4603,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..6947f79d49 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. + dict_T *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..523f8db6f0 --- /dev/null +++ b/src/nvim/shada.c @@ -0,0 +1,4040 @@ +#ifdef HAVE_BE64TOH +# define _BSD_SOURCE 1 +# define _DEFAULT_SOURCE 1 +# include <endian.h> +#endif +#include <stdlib.h> +#include <stddef.h> +#include <stdbool.h> +#include <string.h> +#include <stdint.h> +#include <inttypes.h> +#include <errno.h> +#include <unistd.h> +#include <assert.h> + +#include <msgpack.h> +#include <uv.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/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/buffer.h" +#include "nvim/buffer_defs.h" +#include "nvim/misc2.h" +#include "nvim/ex_getln.h" +#include "nvim/search.h" +#include "nvim/eval.h" +#include "nvim/regexp.h" +#include "nvim/eval_defs.h" +#include "nvim/version.h" +#include "nvim/path.h" +#include "nvim/fileio.h" +#include "nvim/strings.h" +#include "nvim/lib/khash.h" +#include "nvim/lib/kvec.h" + +// Note: when using bufset hash pointers are intentionally casted to uintptr_t +// and not to khint32_t or khint64_t: this way compiler must give a warning +// (-Wconversion) when types change. +#ifdef ARCH_32 +KHASH_SET_INIT_INT(bufset) +#elif defined(ARCH_64) +KHASH_SET_INIT_INT64(bufset) +#else +# error Not a 64- or 32-bit architecture +#endif +KHASH_MAP_INIT_STR(fnamebufs, buf_T *) +KHASH_SET_INIT_STR(strset) + +#define copy_option_part(src, dest, ...) \ + ((char *) copy_option_part((char_u **) src, (char_u *) dest, __VA_ARGS__)) +#define find_shada_parameter(...) \ + ((const char *) find_shada_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 emsgu(a, ...) emsgu((char_u *) a, __VA_ARGS__) +#define home_replace_save(a, b) \ + ((char *)home_replace_save(a, (char_u *)b)) +#define home_replace(a, b, c, d, e) \ + home_replace(a, (char_u *)b, (char_u *)c, d, e) +#define vim_rename(a, b) \ + (vim_rename((char_u *)a, (char_u *)b)) +#define mb_strnicmp(a, b, c) \ + (mb_strnicmp((char_u *)a, (char_u *)b, c)) +#define has_non_ascii(a) (has_non_ascii((char_u *)a)) +#define string_convert(a, b, c) \ + ((char *)string_convert((vimconv_T *)a, (char_u *)b, c)) +#define path_shorten_fname_if_possible(b) \ + ((char *)path_shorten_fname_if_possible((char_u *)b)) +#define buflist_new(ffname, sfname, ...) \ + (buflist_new((char_u *)ffname, (char_u *)sfname, __VA_ARGS__)) +#define convert_setup(vcp, from, to) \ + (convert_setup(vcp, (char_u *)from, (char_u *)to)) +#define os_getperm(f) \ + (os_getperm((char_u *) f)) +#define os_isdir(f) (os_isdir((char_u *) f)) +#define regtilde(s, m) ((char *) regtilde((char_u *) s, m)) +#define path_tail_with_sep(f) ((char *) path_tail_with_sep((char_u *)f)) + +#define SEARCH_KEY_MAGIC "sm" +#define SEARCH_KEY_SMARTCASE "sc" +#define SEARCH_KEY_HAS_LINE_OFFSET "sl" +#define SEARCH_KEY_PLACE_CURSOR_AT_END "se" +#define SEARCH_KEY_IS_LAST_USED "su" +#define SEARCH_KEY_IS_SUBSTITUTE_PATTERN "ss" +#define SEARCH_KEY_HIGHLIGHTED "sh" +#define SEARCH_KEY_OFFSET "so" +#define SEARCH_KEY_PAT "sp" + +#define REG_KEY_TYPE "rt" +#define REG_KEY_WIDTH "rw" +#define REG_KEY_CONTENTS "rc" + +#define KEY_LNUM "l" +#define KEY_COL "c" +#define KEY_FILE "f" +#define KEY_NAME_CHAR "n" + +// Error messages formerly used by viminfo code: +// E136: viminfo: Too many errors, skipping rest of file +// E137: Viminfo file is not writable: %s +// E138: Can't write viminfo file %s! +// E195: Cannot open ShaDa file for reading +// E574: Unknown register type %d +// E575: Illegal starting char +// E576: Missing '>' +// E577: Illegal register name +// E886: Can't rename viminfo file to %s! +// Now only five of them are used: +// E137: ShaDa file is not writeable (for pre-open checks) +// E138: All %s.tmp.X files exist, cannot write ShaDa file! +// RCERR (E576) for critical read errors. +// RNERR (E136) for various errors when renaming. +// RERR (E575) for various errors inside read ShaDa file. +// SERR (E886) for various “system” errors (always contains output of +// strerror) + +/// Common prefix for all errors inside ShaDa file +/// +/// I.e. errors occurred while parsing, but not system errors occurred while +/// reading. +#define RERR "E575: " + +/// Common prefix for critical read errors +/// +/// I.e. errors that make shada_read_next_item return kSDReadStatusNotShaDa. +#define RCERR "E576: " + +/// Common prefix for all “system” errors +#define SERR "E886: " + +/// Common prefix for all “rename” errors +#define RNERR "E136: " + +/// Flags for shada_read_file and children +typedef enum { + kShaDaWantInfo = 1, ///< Load non-mark information + kShaDaWantMarks = 2, ///< Load local file marks and change list + kShaDaForceit = 4, ///< Overwrite info already read + kShaDaGetOldfiles = 8, ///< Load v:oldfiles. + kShaDaMissingError = 16, ///< Error out when os_open returns -ENOENT. +} ShaDaReadFileFlags; + +/// 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. + kSDItemChange = 11, ///< Item from buffer change list. +#define SHADA_LAST_ENTRY ((uint64_t) kSDItemChange) +} ShadaEntryType; + +/// Possible results when reading ShaDa file +typedef enum { + kSDReadStatusSuccess, ///< Reading was successfull. + kSDReadStatusFinished, ///< Nothing more to read. + kSDReadStatusReadError, ///< Failed to read from file. + kSDReadStatusNotShaDa, ///< Input is most likely not a ShaDa file. + kSDReadStatusMalformed, ///< Error in the currently read item. +} ShaDaReadResult; + +/// Possible results of shada_write function. +typedef enum { + kSDWriteSuccessfull, ///< Writing was successfull. + kSDWriteReadNotShada, ///< Writing was successfull, but when reading it + ///< attempted to read file that did not look like + ///< a ShaDa file. + kSDWriteFailed, ///< Writing was not successfull (e.g. because there + ///< was no space left on device). +} ShaDaWriteResult; + +/// Flags for shada_read_next_item +enum SRNIFlags { + kSDReadHeader = (1 << kSDItemHeader), ///< Determines whether header should + ///< be read (it is usually ignored). + kSDReadUndisableableData = ( + (1 << kSDItemSearchPattern) + | (1 << kSDItemSubString) + | (1 << kSDItemJump)), ///< Data reading which cannot be disabled by &shada + ///< or other options except for disabling reading + ///< ShaDa as a whole. + kSDReadRegisters = (1 << kSDItemRegister), ///< Determines whether registers + ///< should be read (may only be + ///< disabled when writing, but + ///< not when reading). + kSDReadHistory = (1 << kSDItemHistoryEntry), ///< Determines whether history + ///< should be read (can only be + ///< disabled by &history). + kSDReadVariables = (1 << kSDItemVariable), ///< Determines whether variables + ///< should be read (disabled by + ///< removing ! from &shada). + kSDReadBufferList = (1 << kSDItemBufferList), ///< Determines whether buffer + ///< list should be read + ///< (disabled by removing + ///< % entry from &shada). + kSDReadUnknown = (1 << (SHADA_LAST_ENTRY + 1)), ///< Determines whether + ///< unknown items should be + ///< read (usually disabled). + kSDReadGlobalMarks = (1 << kSDItemGlobalMark), ///< Determines whether global + ///< marks should be read. Can + ///< only be disabled by + ///< having f0 in &shada when + ///< writing. + kSDReadLocalMarks = (1 << kSDItemLocalMark), ///< Determines whether local + ///< marks should be read. Can + ///< only be disabled by + ///< disabling &shada or putting + ///< '0 there. Is also used for + ///< v:oldfiles. + kSDReadChanges = (1 << kSDItemChange), ///< Determines whether change list + ///< should be read. Can only be + ///< disabled by disabling &shada or + ///< putting '0 there. +}; +// Note: SRNIFlags enum name was created only to make it possible to reference +// it. This name is not actually used anywhere outside of the documentation. + +/// 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; + dict_T *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; + bool highlighted; + char *pat; + dict_T *additional_data; + } search_pattern; + struct history_item { + uint8_t histtype; + char *string; + char sep; + list_T *additional_elements; + } history_item; + struct reg { + char name; + uint8_t type; + char **contents; + size_t contents_size; + size_t width; + dict_T *additional_data; + } reg; + struct global_var { + char *name; + typval_T value; + list_T *additional_elements; + } global_var; + struct { + uint64_t type; + char *contents; + size_t size; + } unknown_item; + struct sub_string { + char *sub; + list_T *additional_elements; + } sub_string; + struct buffer_list { + size_t size; + struct buffer_list_buffer { + pos_T pos; + char *fname; + dict_T *additional_data; + } *buffers; + } buffer_list; + } data; +} ShadaEntry; + +struct hm_llist_entry; + +/// One entry in sized linked list +typedef struct hm_llist_entry { + ShadaEntry data; ///< Entry data. + bool can_free_entry; ///< True if data can be freed. + struct hm_llist_entry *next; ///< Pointer to next entry or NULL. + struct hm_llist_entry *prev; ///< Pointer to previous entry or NULL. +} HMLListEntry; + +KHASH_MAP_INIT_STR(hmll_entries, HMLListEntry *) + +/// Sized linked list structure for history merger +typedef struct { + HMLListEntry *entries; ///< Pointer to the start of the allocated array of + ///< entries. + HMLListEntry *first; ///< First entry in the list (is not necessary start + ///< of the array) or NULL. + HMLListEntry *last; ///< Last entry in the list or NULL. + HMLListEntry *free_entry; ///< Last free entry removed by hmll_remove. + HMLListEntry *last_free_entry; ///< Last unused element in entries array. + size_t size; ///< Number of allocated entries. + size_t num_entries; ///< Number of entries already used. + khash_t(hmll_entries) contained_entries; ///< Hash mapping all history entry + ///< strings to corresponding entry + ///< pointers. +} HMLList; + +typedef struct { + HMLList hmll; + bool do_merge; + bool reading; + const void *iter; + ShadaEntry last_hist_entry; + uint8_t history_type; +} HistoryMergerState; + +/// ShadaEntry structure that knows whether it should be freed +typedef struct { + ShadaEntry data; ///< ShadaEntry data. + bool can_free_entry; ///< True if entry can be freed. +} PossiblyFreedShadaEntry; + +/// Structure that holds one file marks. +typedef struct { + PossiblyFreedShadaEntry marks[NLOCALMARKS]; ///< All file marks. + PossiblyFreedShadaEntry changes[JUMPLISTSIZE]; ///< All file changes. + size_t changes_size; ///< Number of changes occupied. + ShadaEntry *additional_marks; ///< All marks with unknown names. + size_t additional_marks_size; ///< Size of the additional_marks array. + Timestamp greatest_timestamp; ///< Greatest timestamp among marks. +} FileMarks; + +KHASH_MAP_INIT_STR(file_marks, FileMarks) + +/// State structure used by shada_write +/// +/// Before actually writing most of the data is read to this structure. +typedef struct { + HistoryMergerState hms[HIST_COUNT]; ///< Structures for history merging. + PossiblyFreedShadaEntry global_marks[NGLOBALMARKS]; ///< All global marks. + PossiblyFreedShadaEntry registers[NUM_SAVED_REGISTERS]; ///< All registers. + PossiblyFreedShadaEntry jumps[JUMPLISTSIZE]; ///< All dumped jumps. + size_t jumps_size; ///< Number of jumps occupied. + PossiblyFreedShadaEntry search_pattern; ///< Last search pattern. + PossiblyFreedShadaEntry sub_search_pattern; ///< Last s/ search pattern. + PossiblyFreedShadaEntry replacement; ///< Last s// replacement string. + khash_t(strset) dumped_variables; ///< Names of already dumped variables. + khash_t(file_marks) file_marks; ///< All file marks. +} WriteMergerState; + +struct sd_read_def; + +/// Function used to close files defined by ShaDaReadDef +typedef void (*ShaDaReadCloser)(struct sd_read_def *const sd_reader) + REAL_FATTR_NONNULL_ALL; + +/// Function used to read ShaDa files +typedef ptrdiff_t (*ShaDaFileReader)(struct sd_read_def *const sd_reader, + void *const dest, + const size_t size) + REAL_FATTR_NONNULL_ALL REAL_FATTR_WARN_UNUSED_RESULT; + +/// Function used to skip in ShaDa files +typedef int (*ShaDaFileSkipper)(struct sd_read_def *const sd_reader, + const size_t offset) + REAL_FATTR_NONNULL_ALL REAL_FATTR_WARN_UNUSED_RESULT; + +/// Structure containing necessary pointers for reading ShaDa files +typedef struct sd_read_def { + ShaDaFileReader read; ///< Reader function. + ShaDaReadCloser close; ///< Close function. + ShaDaFileSkipper skip; ///< Function used to skip some bytes. + void *cookie; ///< Data describing object read from. + bool eof; ///< True if reader reached end of file. + char *error; ///< Error message in case of error. + uintmax_t fpos; ///< Current position (amount of bytes read since + ///< reader structure initialization). May overflow. + vimconv_T sd_conv; ///< Structure used for converting encodings of some + ///< items. +} ShaDaReadDef; + +struct sd_write_def; + +/// Function used to close files defined by ShaDaWriteDef +typedef void (*ShaDaWriteCloser)(struct sd_write_def *const sd_writer) + REAL_FATTR_NONNULL_ALL; + +/// Function used to write ShaDa files +typedef ptrdiff_t (*ShaDaFileWriter)(struct sd_write_def *const sd_writer, + const void *const src, + const size_t size) + REAL_FATTR_NONNULL_ALL REAL_FATTR_WARN_UNUSED_RESULT; + +/// Structure containing necessary pointers for writing ShaDa files +typedef struct sd_write_def { + ShaDaFileWriter write; ///< Writer function. + ShaDaWriteCloser close; ///< Close function. + void *cookie; ///< Data describing object written to. + char *error; ///< Error message in case of error. + vimconv_T sd_conv; ///< Structure used for converting encodings of some + ///< items. +} ShaDaWriteDef; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "shada.c.generated.h" +#endif + +#define DEF_SDE(name, attr, ...) \ + [kSDItem##name] = { \ + .timestamp = 0, \ + .type = kSDItem##name, \ + .data = { \ + .attr = { __VA_ARGS__ } \ + } \ + } +#define DEFAULT_POS {1, 0, 0} +static const pos_T default_pos = DEFAULT_POS; +static const ShadaEntry sd_default_values[] = { + [kSDItemMissing] = { .type = kSDItemMissing, .timestamp = 0 }, + DEF_SDE(Header, header, .size = 0), + DEF_SDE(SearchPattern, 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, + .highlighted = false, + .pat = NULL, + .additional_data = NULL), + DEF_SDE(SubString, sub_string, .sub = NULL, .additional_elements = NULL), + DEF_SDE(HistoryEntry, history_item, + .histtype = HIST_CMD, + .string = NULL, + .sep = NUL, + .additional_elements = NULL), + DEF_SDE(Register, reg, + .name = NUL, + .type = MCHAR, + .contents = NULL, + .contents_size = 0, + .width = 0, + .additional_data = NULL), + DEF_SDE(Variable, global_var, + .name = NULL, + .value = { + .v_type = VAR_UNKNOWN, + .vval = { .v_string = NULL } + }, + .additional_elements = NULL), + DEF_SDE(GlobalMark, filemark, + .name = '"', + .mark = DEFAULT_POS, + .fname = NULL, + .additional_data = NULL), + DEF_SDE(Jump, filemark, + .name = NUL, + .mark = DEFAULT_POS, + .fname = NULL, + .additional_data = NULL), + DEF_SDE(BufferList, buffer_list, + .size = 0, + .buffers = NULL), + DEF_SDE(LocalMark, filemark, + .name = '"', + .mark = DEFAULT_POS, + .fname = NULL, + .additional_data = NULL), + DEF_SDE(Change, filemark, + .name = NUL, + .mark = DEFAULT_POS, + .fname = NULL, + .additional_data = NULL), +}; +#undef DEFAULT_POS +#undef DEF_SDE + +/// Initialize new linked list +/// +/// @param[out] hmll List to initialize. +/// @param[in] size Maximum size of the list. +static inline void hmll_init(HMLList *const hmll, const size_t size) + FUNC_ATTR_NONNULL_ALL +{ + *hmll = (HMLList) { + .entries = xcalloc(size, sizeof(hmll->entries[0])), + .first = NULL, + .last = NULL, + .free_entry = NULL, + .size = size, + .num_entries = 0, + .contained_entries = KHASH_EMPTY_TABLE(hmll_entries), + }; + hmll->last_free_entry = hmll->entries; +} + +/// Iterate over HMLList in forward direction +/// +/// @param hmll Pointer to the list. +/// @param cur_entry Name of the variable to iterate over. +/// +/// @return `for` cycle header (use `HMLL_FORALL(hmll, cur_entry) {body}`). +#define HMLL_FORALL(hmll, cur_entry) \ + for (HMLListEntry *cur_entry = (hmll)->first; cur_entry != NULL; \ + cur_entry = cur_entry->next) + +/// Remove entry from the linked list +/// +/// @param hmll List to remove from. +/// @param hmll_entry Entry to remove. +static inline void hmll_remove(HMLList *const hmll, + HMLListEntry *const hmll_entry) + FUNC_ATTR_NONNULL_ALL +{ + if (hmll_entry == hmll->last_free_entry - 1) { + hmll->last_free_entry--; + } else { + assert(hmll->free_entry == NULL); + hmll->free_entry = hmll_entry; + } + const khiter_t k = kh_get(hmll_entries, &hmll->contained_entries, + hmll_entry->data.data.history_item.string); + assert(k != kh_end(&hmll->contained_entries)); + kh_del(hmll_entries, &hmll->contained_entries, k); + if (hmll_entry->next == NULL) { + hmll->last = hmll_entry->prev; + } else { + hmll_entry->next->prev = hmll_entry->prev; + } + if (hmll_entry->prev == NULL) { + hmll->first = hmll_entry->next; + } else { + hmll_entry->prev->next = hmll_entry->next; + } + hmll->num_entries--; + if (hmll_entry->can_free_entry) { + shada_free_shada_entry(&hmll_entry->data); + } +} + + +/// Insert entry to the linked list +/// +/// @param[out] hmll List to insert to. +/// @param[in] hmll_entry Entry to insert after or NULL if it is needed +/// to insert at the first entry. +/// @param[in] data Data to insert. +/// @param[in] can_free_entry True if data can be freed. +static inline void hmll_insert(HMLList *const hmll, + HMLListEntry *hmll_entry, + const ShadaEntry data, + const bool can_free_entry) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (hmll->num_entries == hmll->size) { + if (hmll_entry == hmll->first) { + hmll_entry = NULL; + } + hmll_remove(hmll, hmll->first); + } + HMLListEntry *target_entry; + if (hmll->free_entry == NULL) { + assert((size_t) (hmll->last_free_entry - hmll->entries) + == hmll->num_entries); + target_entry = hmll->last_free_entry++; + } else { + assert((size_t) (hmll->last_free_entry - hmll->entries) - 1 + == hmll->num_entries); + target_entry = hmll->free_entry; + hmll->free_entry = NULL; + } + target_entry->data = data; + target_entry->can_free_entry = can_free_entry; + int kh_ret; + const khiter_t k = kh_put(hmll_entries, &hmll->contained_entries, + data.data.history_item.string, &kh_ret); + if (kh_ret > 0) { + kh_val(&hmll->contained_entries, k) = target_entry; + } + hmll->num_entries++; + target_entry->prev = hmll_entry; + if (hmll_entry == NULL) { + target_entry->next = hmll->first; + hmll->first = target_entry; + } else { + target_entry->next = hmll_entry->next; + hmll_entry->next = target_entry; + } + if (target_entry->next == NULL) { + hmll->last = target_entry; + } else { + target_entry->next->prev = target_entry; + } +} + +/// Iterate over HMLList in backward direction +/// +/// @param hmll Pointer to the list. +/// @param cur_entry Name of the variable to iterate over, must be already +/// defined. +/// +/// @return `for` cycle header (use `HMLL_FORALL(hmll, cur_entry) {body}`). +#define HMLL_ITER_BACK(hmll, cur_entry) \ + for (cur_entry = (hmll)->last; cur_entry != NULL; \ + cur_entry = cur_entry->prev) + +/// Free linked list +/// +/// @param[in] hmll List to free. +static inline void hmll_dealloc(HMLList *const hmll) + FUNC_ATTR_NONNULL_ALL +{ + kh_dealloc(hmll_entries, &hmll->contained_entries); + xfree(hmll->entries); +} + +/// Wrapper for reading from file descriptors +/// +/// @return -1 or number of bytes read. +static ptrdiff_t read_file(ShaDaReadDef *const sd_reader, void *const dest, + const size_t size) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + size_t read_bytes = 0; + bool did_try_to_free = false; + const int fd = (int)(intptr_t) sd_reader->cookie; + while (read_bytes != size) { + const ptrdiff_t cur_read_bytes = read(fd, ((char *) dest) + read_bytes, + size - read_bytes); + if (cur_read_bytes > 0) { + read_bytes += (size_t) cur_read_bytes; + sd_reader->fpos += (uintmax_t) cur_read_bytes; + assert(read_bytes <= size); + } + if (cur_read_bytes < 0) { + if (errno == EINTR || errno == EAGAIN) { + errno = 0; + continue; + } else if (errno == ENOMEM && !did_try_to_free) { + try_to_free_memory(); + did_try_to_free = true; + errno = 0; + continue; + } else { + sd_reader->error = strerror(errno); + errno = 0; + return -1; + } + } + if (cur_read_bytes == 0) { + sd_reader->eof = true; + break; + } + } + return (ptrdiff_t) read_bytes; +} + +/// Read one character +static int read_char(ShaDaReadDef *const sd_reader) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + uint8_t ret; + ptrdiff_t read_bytes = sd_reader->read(sd_reader, &ret, 1); + if (read_bytes != 1) { + return EOF; + } + return (int) ret; +} + +/// Wrapper for writing to file descriptors +/// +/// @return -1 or number of bytes written. +static ptrdiff_t write_file(ShaDaWriteDef *const sd_writer, + const void *const dest, + const size_t size) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + size_t written_bytes = 0; + const int fd = (int)(intptr_t) sd_writer->cookie; + while (written_bytes != size) { + const ptrdiff_t cur_written_bytes = write(fd, (char *) dest + written_bytes, + size - written_bytes); + if (cur_written_bytes > 0) { + written_bytes += (size_t) cur_written_bytes; + } + if (cur_written_bytes < 0) { + if (errno == EINTR || errno == EAGAIN) { + errno = 0; + continue; + } else { + sd_writer->error = strerror(errno); + errno = 0; + return -1; + } + } + if (cur_written_bytes == 0) { + sd_writer->error = "Zero bytes written."; + return -1; + } + } + return (ptrdiff_t) written_bytes; +} + +/// Wrapper for closing file descriptors opened for reading +static void close_sd_reader(ShaDaReadDef *const sd_reader) + FUNC_ATTR_NONNULL_ALL +{ + close_file((int)(intptr_t) sd_reader->cookie); +} + +/// Wrapper for closing file descriptors opened for writing +static void close_sd_writer(ShaDaWriteDef *const sd_writer) + FUNC_ATTR_NONNULL_ALL +{ + const int fd = (int)(intptr_t) sd_writer->cookie; + if (fsync(fd) < 0) { + emsg2(_(SERR "System error while synchronizing ShaDa file: %s"), + strerror(errno)); + errno = 0; + } + close_file(fd); +} + +/// Wrapper for read that reads to IObuff and ignores bytes read +/// +/// Used for skipping. +/// +/// @param[in,out] sd_reader File read. +/// @param[in] offset Amount of bytes to skip. +/// +/// @return FAIL in case of failure, OK in case of success. May set +/// sd_reader->eof or sd_reader->error. +static int sd_reader_skip_read(ShaDaReadDef *const sd_reader, + const size_t offset) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + size_t read_bytes = 0; + do { + ptrdiff_t new_read_bytes = sd_reader->read( + sd_reader, IObuff, (size_t) (offset - read_bytes > IOSIZE + ? IOSIZE + : offset - read_bytes)); + if (new_read_bytes == -1) { + return FAIL; + } + read_bytes += (size_t) new_read_bytes; + } while (read_bytes < offset && !sd_reader->eof); + + return (read_bytes == offset ? OK : FAIL); +} + +/// Wrapper for read that can be used when lseek cannot be used +/// +/// E.g. when trying to read from a pipe. +/// +/// @param[in,out] sd_reader File read. +/// @param[in] offset Amount of bytes to skip. +/// +/// @return kSDReadStatusReadError, kSDReadStatusNotShaDa or +/// kSDReadStatusSuccess. +static ShaDaReadResult sd_reader_skip(ShaDaReadDef *const sd_reader, + const size_t offset) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + if (sd_reader->skip(sd_reader, offset) != OK) { + if (sd_reader->error != NULL) { + emsg2(_(SERR "System error while skipping in ShaDa file: %s"), + sd_reader->error); + return kSDReadStatusReadError; + } else if (sd_reader->eof) { + emsgu(_(RCERR "Error while reading ShaDa file: " + "last entry specified that it occupies %" PRIu64 " bytes, " + "but file ended earlier"), + (uint64_t) offset); + return kSDReadStatusNotShaDa; + } + assert(false); + } + return kSDReadStatusSuccess; +} + +/// Wrapper for opening file descriptors +/// +/// All arguments are passed to os_open(). +/// +/// @return file descriptor or -errno on failure. +static int open_file(const char *const fname, const int flags, const int mode) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + bool did_try_to_free = false; + int fd; +open_file_start: + fd = os_open(fname, flags, mode); + + if (fd < 0) { + if (-fd == ENOENT) { + return fd; + } + if (-fd == ENOMEM && !did_try_to_free) { + try_to_free_memory(); + did_try_to_free = true; + goto open_file_start; + } + if (-fd != EEXIST) { + emsg3(_(SERR "System error while opening ShaDa file %s: %s"), + fname, os_strerror(fd)); + } + return fd; + } + return fd; +} + +/// Open ShaDa file for reading +/// +/// @param[in] fname File name to open. +/// @param[out] sd_reader Location where reader structure will be saved. +/// +/// @return -errno in case of error, 0 otherwise. +static int open_shada_file_for_reading(const char *const fname, + ShaDaReadDef *sd_reader) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + const intptr_t fd = (intptr_t) open_file(fname, O_RDONLY, 0); + + if (fd < 0) { + return (int) fd; + } + + *sd_reader = (ShaDaReadDef) { + .read = &read_file, + .close = &close_sd_reader, + .skip = &sd_reader_skip_read, + .error = NULL, + .eof = false, + .fpos = 0, + .cookie = (void *) fd, + }; + + convert_setup(&sd_reader->sd_conv, "utf-8", p_enc); + + return 0; +} + +/// Wrapper for closing file descriptors +static void close_file(int fd) +{ +close_file_start: + if (close(fd) == -1) { + if (errno == EINTR) { + errno = 0; + goto close_file_start; + } else { + emsg2(_(SERR "System error while closing ShaDa file: %s"), + strerror(errno)); + errno = 0; + } + } +} + +/// Check whether buffer is in the given set +/// +/// @param[in] set Set to check within. +/// @param[in] buf Buffer to find. +/// +/// @return true or false. +static inline bool in_bufset(const khash_t(bufset) *const set, const buf_T *buf) + FUNC_ATTR_PURE +{ + return kh_get(bufset, set, (uintptr_t) buf) != kh_end(set); +} + +/// Check whether string is in the given set +/// +/// @param[in] set Set to check within. +/// @param[in] buf Buffer to find. +/// +/// @return true or false. +static inline bool in_strset(const khash_t(strset) *const set, char *str) + FUNC_ATTR_PURE +{ + return kh_get(strset, set, str) != kh_end(set); +} + +/// Msgpack callback for writing to ShaDaWriteDef* +static int msgpack_sd_writer_write(void *data, const char *buf, size_t len) +{ + ShaDaWriteDef *const sd_writer = (ShaDaWriteDef *) data; + ptrdiff_t written_bytes = sd_writer->write(sd_writer, buf, len); + if (written_bytes == -1) { + emsg2(_(SERR "System error while writing ShaDa file: %s"), + sd_writer->error); + return -1; + } + return 0; +} + +/// 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 ShaDaReadFileFlags enum. +/// +/// @return FAIL if reading failed for some reason and OK otherwise. +static int shada_read_file(const char *const file, const int flags) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (shada_disabled()) { + return FAIL; + } + + char *const fname = shada_filename(file); + + ShaDaReadDef sd_reader; + const int of_ret = open_shada_file_for_reading(fname, &sd_reader); + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Reading ShaDa file \"%s\"%s%s%s"), + fname, + (flags & kShaDaWantInfo) ? _(" info") : "", + (flags & kShaDaWantMarks) ? _(" marks") : "", + (flags & kShaDaGetOldfiles) ? _(" oldfiles") : "", + of_ret != 0 ? _(" FAILED") : ""); + verbose_leave(); + } + + if (of_ret != 0) { + if (-of_ret == ENOENT && (flags & kShaDaMissingError)) { + emsg3(_(SERR "System error while opening ShaDa file %s for reading: %s"), + fname, os_strerror(of_ret)); + } + xfree(fname); + return FAIL; + } + xfree(fname); + + shada_read(&sd_reader, flags); + sd_reader.close(&sd_reader); + + return OK; +} + +/// Wrapper for hist_iter() function which produces ShadaEntry values +/// +/// @param[in] iter Current iteration state. +/// @param[in] history_type Type of the history (HIST_*). +/// @param[in] zero If true, then item is removed from instance +/// memory upon reading. +/// @param[out] hist Location where iteration results should be saved. +/// +/// @return Next iteration state. +static const void *shada_hist_iter(const void *const iter, + const uint8_t history_type, + const bool zero, + ShadaEntry *const hist) + FUNC_ATTR_NONNULL_ARG(4) FUNC_ATTR_WARN_UNUSED_RESULT +{ + histentry_T hist_he; + const void *const ret = hist_iter(iter, history_type, zero, &hist_he); + if (hist_he.hisstr == NULL) { + *hist = (ShadaEntry) { .type = kSDItemMissing }; + } else { + *hist = (ShadaEntry) { + .type = kSDItemHistoryEntry, + .timestamp = hist_he.timestamp, + .data = { + .history_item = { + .histtype = history_type, + .string = (char *) hist_he.hisstr, + .sep = (char) (history_type == HIST_SEARCH + ? (char) hist_he.hisstr[STRLEN(hist_he.hisstr) + 1] + : 0), + .additional_elements = hist_he.additional_elements, + } + } + }; + } + return ret; +} + +/// Insert history entry +/// +/// Inserts history entry at the end of the ring buffer (may insert earlier +/// according to the timestamp). If entry was already in the ring buffer +/// existing entry will be removed unless it has greater timestamp. +/// +/// Before the new entry entries from the current Neovim history will be +/// inserted unless `do_iter` argument is false. +/// +/// @param[in,out] hms_p Ring buffer and associated structures. +/// @param[in] entry Inserted entry. +/// @param[in] do_iter Determines whether Neovim own history should +/// be used. Must be true only if inserting +/// entry from current Neovim history. +/// @param[in] can_free_entry True if entry can be freed. +static void hms_insert(HistoryMergerState *const hms_p, const ShadaEntry entry, + const bool do_iter, const bool can_free_entry) + FUNC_ATTR_NONNULL_ALL +{ + if (do_iter) { + while (hms_p->last_hist_entry.type != kSDItemMissing + && hms_p->last_hist_entry.timestamp < entry.timestamp) { + hms_insert(hms_p, hms_p->last_hist_entry, false, hms_p->reading); + if (hms_p->iter == NULL) { + hms_p->last_hist_entry.type = kSDItemMissing; + break; + } + hms_p->iter = shada_hist_iter(hms_p->iter, hms_p->history_type, + hms_p->reading, &hms_p->last_hist_entry); + } + } + HMLList *const hmll = &hms_p->hmll; + const khiter_t k = kh_get(hmll_entries, &hms_p->hmll.contained_entries, + entry.data.history_item.string); + if (k != kh_end(&hmll->contained_entries)) { + HMLListEntry *const existing_entry = kh_val(&hmll->contained_entries, k); + if (entry.timestamp > existing_entry->data.timestamp) { + hmll_remove(hmll, existing_entry); + } else if (!do_iter && entry.timestamp == existing_entry->data.timestamp) { + // Prefer entry from the current Neovim instance. + if (existing_entry->can_free_entry) { + shada_free_shada_entry(&existing_entry->data); + } + existing_entry->data = entry; + existing_entry->can_free_entry = can_free_entry; + // Previous key was freed above, as part of freeing the ShaDa entry. + kh_key(&hmll->contained_entries, k) = entry.data.history_item.string; + return; + } else { + return; + } + } + HMLListEntry *insert_after; + HMLL_ITER_BACK(hmll, insert_after) { + if (insert_after->data.timestamp <= entry.timestamp) { + break; + } + } + hmll_insert(hmll, insert_after, entry, can_free_entry); +} + +/// Initialize the history merger +/// +/// @param[out] hms_p Structure to be initialized. +/// @param[in] history_type History type (one of HIST_\* values). +/// @param[in] num_elements Number of elements in the result. +/// @param[in] do_merge Prepare structure for merging elements. +/// @param[in] reading If true, then merger is reading history for use +/// in Neovim. +static inline void hms_init(HistoryMergerState *const hms_p, + const uint8_t history_type, + const size_t num_elements, + const bool do_merge, + const bool reading) + FUNC_ATTR_NONNULL_ALL +{ + hmll_init(&hms_p->hmll, num_elements); + hms_p->do_merge = do_merge; + hms_p->reading = reading; + hms_p->iter = shada_hist_iter(NULL, history_type, hms_p->reading, + &hms_p->last_hist_entry); + hms_p->history_type = history_type; +} + +/// Merge in all remaining Neovim own history entries +/// +/// @param[in,out] hms_p Merger structure into which history should be +/// inserted. +static inline void hms_insert_whole_neovim_history( + HistoryMergerState *const hms_p) + FUNC_ATTR_NONNULL_ALL +{ + while (hms_p->last_hist_entry.type != kSDItemMissing) { + hms_insert(hms_p, hms_p->last_hist_entry, false, hms_p->reading); + if (hms_p->iter == NULL) { + break; + } + hms_p->iter = shada_hist_iter(hms_p->iter, hms_p->history_type, + hms_p->reading, &hms_p->last_hist_entry); + } +} + +/// Convert merger structure to Neovim internal structure for history +/// +/// @param[in] hms_p Converted merger structure. +/// @param[out] hist_array Array with the results. +/// @param[out] new_hisidx New last history entry index. +/// @param[out] new_hisnum Amount of history items in merger structure. +static inline void hms_to_he_array(const HistoryMergerState *const hms_p, + histentry_T *const hist_array, + int *const new_hisidx, + int *const new_hisnum) + FUNC_ATTR_NONNULL_ALL +{ + histentry_T *hist = hist_array; + HMLL_FORALL(&hms_p->hmll, cur_entry) { + hist->timestamp = cur_entry->data.timestamp; + hist->hisnum = (int) (hist - hist_array) + 1; + hist->hisstr = (char_u *) cur_entry->data.data.history_item.string; + hist->additional_elements = + cur_entry->data.data.history_item.additional_elements; + hist++; + } + *new_hisnum = (int) (hist - hist_array); + *new_hisidx = *new_hisnum - 1; +} + +/// Free history merger structure +/// +/// @param[in] hms_p Structure to be freed. +static inline void hms_dealloc(HistoryMergerState *const hms_p) + FUNC_ATTR_NONNULL_ALL +{ + hmll_dealloc(&hms_p->hmll); +} + +/// Iterate over all history entries in history merger, in order +/// +/// @param[in] hms_p Merger structure to iterate over. +/// @param[out] cur_entry Name of the iterator variable. +/// +/// @return for cycle header. Use `HMS_ITER(hms_p, cur_entry) {body}`. +#define HMS_ITER(hms_p, cur_entry) \ + HMLL_FORALL(&((hms_p)->hmll), cur_entry) + +/// Find buffer for given buffer name (cached) +/// +/// @param[in,out] fname_bufs Cache containing fname to buffer mapping. +/// @param[in] fname File name to find. +/// +/// @return Pointer to the buffer or NULL. +static buf_T *find_buffer(khash_t(fnamebufs) *const fname_bufs, + const char *const fname) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + int kh_ret; + khint_t k = kh_put(fnamebufs, fname_bufs, fname, &kh_ret); + if (!kh_ret) { + return kh_val(fname_bufs, k); + } + kh_key(fname_bufs, k) = xstrdup(fname); + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL) { + if (fnamecmp(fname, buf->b_ffname) == 0) { + kh_val(fname_bufs, k) = buf; + return buf; + } + } + } + kh_val(fname_bufs, k) = NULL; + return NULL; +} + +/// Compare two marks +static inline bool marks_equal(const pos_T a, const pos_T b) +{ + return (a.lnum == b.lnum) && (a.col == b.col); +} + +#define MERGE_JUMPS(jumps_size, jumps, jumps_type, timestamp_attr, mark_attr, \ + entry, fname_cond, free_func, fin_func, \ + idxadj_func, afterfree_func) \ + do { \ + const int jl_len = (int) jumps_size; \ + int i; \ + for (i = jl_len; i > 0; i--) { \ + const jumps_type jl_entry = jumps[i - 1]; \ + if (jl_entry.timestamp_attr <= entry.timestamp) { \ + if (marks_equal(jl_entry.mark_attr, entry.data.filemark.mark) \ + && fname_cond) { \ + i = -1; \ + } \ + break; \ + } \ + } \ + if (i > 0) { \ + if (jl_len == JUMPLISTSIZE) { \ + free_func(jumps[0]); \ + i--; \ + if (i > 0) { \ + memmove(&jumps[0], &jumps[1], sizeof(jumps[1]) * (size_t) i); \ + } \ + } else if (i != jl_len) { \ + memmove(&jumps[i + 1], &jumps[i], \ + sizeof(jumps[0]) * (size_t) (jl_len - i)); \ + } \ + } else if (i == 0) { \ + if (jl_len == JUMPLISTSIZE) { \ + i = -1; \ + } else if (jl_len > 0) { \ + memmove(&jumps[1], &jumps[0], sizeof(jumps[0]) * (size_t) jl_len); \ + } \ + } \ + if (i != -1) { \ + jumps[i] = fin_func(entry); \ + if (jl_len < JUMPLISTSIZE) { \ + jumps_size++; \ + } \ + idxadj_func(i); \ + } else { \ + shada_free_shada_entry(&entry); \ + afterfree_func(entry); \ + } \ + } while (0) + +/// Read data from ShaDa file +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[in] flags What to read, see ShaDaReadFileFlags enum. +static void shada_read(ShaDaReadDef *const sd_reader, const int flags) + FUNC_ATTR_NONNULL_ALL +{ + list_T *oldfiles_list = get_vim_var_list(VV_OLDFILES); + const bool force = flags & kShaDaForceit; + const bool get_old_files = (flags & (kShaDaGetOldfiles | kShaDaForceit) + && (force || oldfiles_list == NULL + || oldfiles_list->lv_len == 0)); + const bool want_marks = flags & kShaDaWantMarks; + const unsigned srni_flags = (unsigned) ( + (flags & kShaDaWantInfo + ? (kSDReadUndisableableData + | kSDReadRegisters + | kSDReadGlobalMarks + | (p_hi ? kSDReadHistory : 0) + | (find_shada_parameter('!') != NULL + ? kSDReadVariables + : 0) + | (find_shada_parameter('%') != NULL + && ARGCOUNT == 0 + ? kSDReadBufferList + : 0)) + : 0) + | (want_marks && get_shada_parameter('\'') > 0 + ? kSDReadLocalMarks | kSDReadChanges + : 0) + | (get_old_files + ? kSDReadLocalMarks + : 0)); + if (srni_flags == 0) { + // Nothing to do. + return; + } + HistoryMergerState hms[HIST_COUNT]; + if (srni_flags & kSDReadHistory) { + for (uint8_t i = 0; i < HIST_COUNT; i++) { + hms_init(&hms[i], i, (size_t) p_hi, true, true); + } + } + ShadaEntry cur_entry; + khash_t(bufset) cl_bufs = KHASH_EMPTY_TABLE(bufset); + khash_t(fnamebufs) fname_bufs = KHASH_EMPTY_TABLE(fnamebufs); + khash_t(strset) oldfiles_set = KHASH_EMPTY_TABLE(strset); + if (get_old_files && (oldfiles_list == NULL || force)) { + oldfiles_list = list_alloc(); + set_vim_var_list(VV_OLDFILES, oldfiles_list); + } + ShaDaReadResult srni_ret; + while ((srni_ret = shada_read_next_item(sd_reader, &cur_entry, srni_flags, 0)) + != kSDReadStatusFinished) { + switch (srni_ret) { + case kSDReadStatusSuccess: { + break; + } + case kSDReadStatusFinished: { + // Should be handled by the while condition. + assert(false); + } + case kSDReadStatusNotShaDa: + case kSDReadStatusReadError: { + goto shada_read_main_cycle_end; + } + case kSDReadStatusMalformed: { + continue; + } + } + switch (cur_entry.type) { + case kSDItemMissing: { + assert(false); + } + case kSDItemUnknown: { + break; + } + case kSDItemHeader: { + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemSearchPattern: { + if (!force) { + SearchPattern pat; + (cur_entry.data.search_pattern.is_substitute_pattern + ? &get_substitute_pattern + : &get_search_pattern)(&pat); + if (pat.pat != NULL && pat.timestamp >= cur_entry.timestamp) { + 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); + } + if (cur_entry.data.search_pattern.is_last_used) { + SET_NO_HLSEARCH(!cur_entry.data.search_pattern.highlighted); + } + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemSubString: { + if (!force) { + SubReplacementString sub; + sub_get_replacement(&sub); + if (sub.sub != NULL && sub.timestamp >= cur_entry.timestamp) { + 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, + }); + // Without using regtilde and without / &cpo flag previous substitute + // string is close to useless: you can only use it with :& or :~ and + // that’s all because s//~ is not available until the first call to + // regtilde. Vim was not calling this for some reason. + (void) regtilde(cur_entry.data.sub_string.sub, p_magic); + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemHistoryEntry: { + if (cur_entry.data.history_item.histtype >= HIST_COUNT) { + shada_free_shada_entry(&cur_entry); + break; + } + hms_insert(hms + cur_entry.data.history_item.histtype, cur_entry, true, + true); + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemRegister: { + 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; + } + if (!force) { + const yankreg_T *const reg = op_register_get(cur_entry.data.reg.name); + if (reg == NULL || reg->timestamp >= cur_entry.timestamp) { + shada_free_shada_entry(&cur_entry); + break; + } + } + if (!op_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, + })) { + shada_free_shada_entry(&cur_entry); + } + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemVariable: { + var_set_global(cur_entry.data.global_var.name, + cur_entry.data.global_var.value); + cur_entry.data.global_var.value.v_type = VAR_UNKNOWN; + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemJump: + case kSDItemGlobalMark: { + buf_T *buf = find_buffer(&fname_bufs, cur_entry.data.filemark.fname); + if (buf != NULL) { + xfree(cur_entry.data.filemark.fname); + cur_entry.data.filemark.fname = NULL; + } + xfmark_T fm = (xfmark_T) { + .fname = (char_u *) (buf == NULL + ? cur_entry.data.filemark.fname + : NULL), + .fmark = { + .mark = cur_entry.data.filemark.mark, + .fnum = (buf == NULL ? 0 : buf->b_fnum), + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.filemark.additional_data, + }, + }; + if (cur_entry.type == kSDItemGlobalMark) { + if (!mark_set_global(cur_entry.data.filemark.name, fm, !force)) { + shada_free_shada_entry(&cur_entry); + break; + } + } else { +#define SDE_TO_XFMARK(entry) fm +#define ADJUST_IDX(i) \ + if (curwin->w_jumplistidx >= i \ + && curwin->w_jumplistidx + 1 <= curwin->w_jumplistlen) { \ + curwin->w_jumplistidx++; \ + } +#define DUMMY_AFTERFREE(entry) + MERGE_JUMPS(curwin->w_jumplistlen, curwin->w_jumplist, xfmark_T, + fmark.timestamp, fmark.mark, cur_entry, + (buf == NULL + ? (jl_entry.fname != NULL + && STRCMP(fm.fname, jl_entry.fname) == 0) + : fm.fmark.fnum == jl_entry.fmark.fnum), + free_xfmark, SDE_TO_XFMARK, ADJUST_IDX, DUMMY_AFTERFREE); +#undef SDE_TO_XFMARK +#undef ADJUST_IDX +#undef DUMMY_AFTERFREE + } + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemBufferList: { + for (size_t i = 0; i < cur_entry.data.buffer_list.size; i++) { + char *const sfname = path_shorten_fname_if_possible( + cur_entry.data.buffer_list.buffers[i].fname); + buf_T *const buf = buflist_new( + cur_entry.data.buffer_list.buffers[i].fname, sfname, 0, + BLN_LISTED); + if (buf != NULL) { + RESET_FMARK(&buf->b_last_cursor, + cur_entry.data.buffer_list.buffers[i].pos, 0); + buflist_setfpos(buf, curwin, buf->b_last_cursor.mark.lnum, + buf->b_last_cursor.mark.col, false); + buf->additional_data = + cur_entry.data.buffer_list.buffers[i].additional_data; + cur_entry.data.buffer_list.buffers[i].additional_data = NULL; + } + } + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemChange: + case kSDItemLocalMark: { + if (get_old_files && !in_strset(&oldfiles_set, + cur_entry.data.filemark.fname)) { + char *fname = cur_entry.data.filemark.fname; + if (want_marks) { + // Do not bother with allocating memory for the string if already + // allocated string from cur_entry can be used. It cannot be used if + // want_marks is set because this way it may be used for a mark. + fname = xstrdup(fname); + } + int kh_ret; + (void) kh_put(strset, &oldfiles_set, fname, &kh_ret); + list_append_allocated_string(oldfiles_list, fname); + if (!want_marks) { + // Avoid free because this string was already used. + cur_entry.data.filemark.fname = NULL; + } + } + if (!want_marks) { + shada_free_shada_entry(&cur_entry); + break; + } + buf_T *buf = find_buffer(&fname_bufs, cur_entry.data.filemark.fname); + if (buf == NULL) { + shada_free_shada_entry(&cur_entry); + break; + } + const fmark_T fm = (fmark_T) { + .mark = cur_entry.data.filemark.mark, + .fnum = 0, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.filemark.additional_data, + }; + if (cur_entry.type == kSDItemLocalMark) { + if (!mark_set_local(cur_entry.data.filemark.name, buf, fm, !force)) { + shada_free_shada_entry(&cur_entry); + break; + } + } else { + int kh_ret; + (void) kh_put(bufset, &cl_bufs, (uintptr_t) buf, &kh_ret); +#define SDE_TO_FMARK(entry) fm +#define AFTERFREE(entry) (entry).data.filemark.fname = NULL +#define DUMMY_IDX_ADJ(i) + MERGE_JUMPS(buf->b_changelistlen, buf->b_changelist, fmark_T, + timestamp, mark, cur_entry, true, + free_fmark, SDE_TO_FMARK, DUMMY_IDX_ADJ, AFTERFREE); +#undef SDE_TO_FMARK +#undef AFTERFREE +#undef DUMMY_IDX_ADJ + } + // Do not free shada entry: except for fname, its allocated memory (i.e. + // additional_data attribute contents if non-NULL) was saved above. + xfree(cur_entry.data.filemark.fname); + break; + } + } + } +shada_read_main_cycle_end: + // Warning: shada_hist_iter returns ShadaEntry elements which use strings from + // original history list. This means that once such entry is removed + // from the history Neovim array will no longer be valid. To reduce + // amount of memory allocations ShaDa file reader allocates enough + // memory for the history string itself and separator character which + // may be assigned right away. + if (srni_flags & kSDReadHistory) { + for (uint8_t i = 0; i < HIST_COUNT; i++) { + hms_insert_whole_neovim_history(&hms[i]); + clr_history(i); + int *new_hisidx; + int *new_hisnum; + histentry_T *hist = hist_get_array(i, &new_hisidx, &new_hisnum); + if (hist != NULL) { + hms_to_he_array(&hms[i], hist, new_hisidx, new_hisnum); + } + hms_dealloc(&hms[i]); + } + } + if (cl_bufs.n_occupied) { + FOR_ALL_TAB_WINDOWS(tp, wp) { + (void) tp; + if (in_bufset(&cl_bufs, wp->w_buffer)) { + wp->w_changelistidx = wp->w_buffer->b_changelistlen; + } + } + } + kh_dealloc(bufset, &cl_bufs); + const char *key; + kh_foreach_key(&fname_bufs, key, { + xfree((void *) key); + }) + kh_dealloc(fnamebufs, &fname_bufs); + kh_dealloc(strset, &oldfiles_set); +} + +/// 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 'shada' 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_shada_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) +#define PACK_STRING(s) \ + do { \ + const String s_ = (s); \ + msgpack_pack_str(spacker, s_.size); \ + msgpack_pack_str_body(spacker, s_.data, s_.size); \ + } while (0) +#define PACK_BIN(s) \ + do { \ + const String s_ = (s); \ + msgpack_pack_bin(spacker, s_.size); \ + msgpack_pack_bin_body(spacker, s_.data, s_.size); \ + } 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 bool shada_pack_entry(msgpack_packer *const packer, + ShadaEntry entry, + const size_t max_kbyte) + FUNC_ATTR_NONNULL_ALL +{ + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + msgpack_packer *spacker = msgpack_packer_new(&sbuf, &msgpack_sbuffer_write); +#define DUMP_ADDITIONAL_ELEMENTS(src) \ + do { \ + if ((src) != NULL) { \ + for (listitem_T *li = (src)->lv_first; li != NULL; li = li->li_next) { \ + if (vim_to_msgpack(spacker, &li->li_tv) == FAIL) { \ + goto shada_pack_entry_error; \ + } \ + } \ + } \ + } while (0) +#define DUMP_ADDITIONAL_DATA(src) \ + do { \ + dict_T *const d = (src); \ + if (d != NULL) { \ + size_t todo = d->dv_hashtab.ht_used; \ + for (const hashitem_T *hi= d->dv_hashtab.ht_array; todo; hi++) { \ + if (!HASHITEM_EMPTY(hi)) { \ + todo--; \ + dictitem_T *const di = HI2DI(hi); \ + const size_t key_len = strlen((const char *) hi->hi_key); \ + msgpack_pack_str(spacker, key_len); \ + msgpack_pack_str_body(spacker, (const char *) hi->hi_key, key_len); \ + if (vim_to_msgpack(spacker, &di->di_tv) == FAIL) { \ + goto shada_pack_entry_error; \ + } \ + } \ + } \ + } \ + } while (0) +#define CHECK_DEFAULT(entry, attr) \ + (sd_default_values[entry.type].data.attr == entry.data.attr) +#define ONE_IF_NOT_DEFAULT(entry, attr) \ + ((size_t) (!CHECK_DEFAULT(entry, attr))) + switch (entry.type) { + case kSDItemMissing: { + assert(false); + } + case kSDItemUnknown: { + if (spacker->callback(spacker->data, entry.data.unknown_item.contents, + (unsigned) entry.data.unknown_item.size) == -1) { + goto shada_pack_entry_error; + } + break; + } + case kSDItemHistoryEntry: { + const bool is_hist_search = + entry.data.history_item.histtype == HIST_SEARCH; + const size_t arr_size = 2 + (size_t) is_hist_search + (size_t) ( + entry.data.history_item.additional_elements == NULL + ? 0 + : entry.data.history_item.additional_elements->lv_len); + msgpack_pack_array(spacker, arr_size); + msgpack_pack_uint8(spacker, entry.data.history_item.histtype); + PACK_BIN(cstr_as_string(entry.data.history_item.string)); + if (is_hist_search) { + msgpack_pack_uint8(spacker, (uint8_t) entry.data.history_item.sep); + } + DUMP_ADDITIONAL_ELEMENTS(entry.data.history_item.additional_elements); + break; + } + case kSDItemVariable: { + const size_t arr_size = 2 + (size_t) ( + entry.data.global_var.additional_elements == NULL + ? 0 + : entry.data.global_var.additional_elements->lv_len); + msgpack_pack_array(spacker, arr_size); + PACK_BIN(cstr_as_string(entry.data.global_var.name)); + if (vim_to_msgpack(spacker, &entry.data.global_var.value) == FAIL) { + goto shada_pack_entry_error; + } + DUMP_ADDITIONAL_ELEMENTS(entry.data.global_var.additional_elements); + break; + } + case kSDItemSubString: { + const size_t arr_size = 1 + (size_t) ( + entry.data.sub_string.additional_elements == NULL + ? 0 + : entry.data.sub_string.additional_elements->lv_len); + msgpack_pack_array(spacker, arr_size); + PACK_BIN(cstr_as_string(entry.data.sub_string.sub)); + DUMP_ADDITIONAL_ELEMENTS(entry.data.sub_string.additional_elements); + break; + } + case kSDItemSearchPattern: { + const size_t map_size = (size_t) ( + 1 // Search pattern is always present + + ONE_IF_NOT_DEFAULT(entry, search_pattern.magic) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.is_last_used) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.smartcase) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.has_line_offset) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.place_cursor_at_end) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.is_substitute_pattern) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.highlighted) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.offset) + // finally, additional data: + + (size_t) ( + entry.data.search_pattern.additional_data + ? entry.data.search_pattern.additional_data->dv_hashtab.ht_used + : 0)); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR(SEARCH_KEY_PAT); + PACK_BIN(cstr_as_string(entry.data.search_pattern.pat)); +#define PACK_BOOL(entry, name, attr) \ + do { \ + if (!CHECK_DEFAULT(entry, search_pattern.attr)) { \ + PACK_STATIC_STR(name); \ + if (sd_default_values[entry.type].data.search_pattern.attr) { \ + msgpack_pack_false(spacker); \ + } else { \ + msgpack_pack_true(spacker); \ + } \ + } \ + } while (0) + PACK_BOOL(entry, SEARCH_KEY_MAGIC, magic); + PACK_BOOL(entry, SEARCH_KEY_IS_LAST_USED, is_last_used); + PACK_BOOL(entry, SEARCH_KEY_SMARTCASE, smartcase); + PACK_BOOL(entry, SEARCH_KEY_HAS_LINE_OFFSET, has_line_offset); + PACK_BOOL(entry, SEARCH_KEY_PLACE_CURSOR_AT_END, place_cursor_at_end); + PACK_BOOL(entry, SEARCH_KEY_IS_SUBSTITUTE_PATTERN, is_substitute_pattern); + PACK_BOOL(entry, SEARCH_KEY_HIGHLIGHTED, highlighted); + if (!CHECK_DEFAULT(entry, search_pattern.offset)) { + PACK_STATIC_STR(SEARCH_KEY_OFFSET); + msgpack_pack_int64(spacker, entry.data.search_pattern.offset); + } +#undef PACK_BOOL + DUMP_ADDITIONAL_DATA(entry.data.search_pattern.additional_data); + break; + } + case kSDItemChange: + case kSDItemGlobalMark: + case kSDItemLocalMark: + case kSDItemJump: { + const size_t map_size = (size_t) ( + 1 // File name + + ONE_IF_NOT_DEFAULT(entry, filemark.mark.lnum) + + ONE_IF_NOT_DEFAULT(entry, filemark.mark.col) + + ONE_IF_NOT_DEFAULT(entry, filemark.name) + // Additional entries, if any: + + (size_t) ( + entry.data.filemark.additional_data == NULL + ? 0 + : entry.data.filemark.additional_data->dv_hashtab.ht_used)); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR(KEY_FILE); + PACK_BIN(cstr_as_string(entry.data.filemark.fname)); + if (!CHECK_DEFAULT(entry, filemark.mark.lnum)) { + PACK_STATIC_STR(KEY_LNUM); + msgpack_pack_long(spacker, entry.data.filemark.mark.lnum); + } + if (!CHECK_DEFAULT(entry, filemark.mark.col)) { + PACK_STATIC_STR(KEY_COL); + msgpack_pack_long(spacker, entry.data.filemark.mark.col); + } + assert(entry.type == kSDItemJump || entry.type == kSDItemChange + ? CHECK_DEFAULT(entry, filemark.name) + : true); + if (!CHECK_DEFAULT(entry, filemark.name)) { + PACK_STATIC_STR(KEY_NAME_CHAR); + msgpack_pack_uint8(spacker, (uint8_t) entry.data.filemark.name); + } + DUMP_ADDITIONAL_DATA(entry.data.filemark.additional_data); + break; + } + case kSDItemRegister: { + const size_t map_size = (size_t) ( + 2 // Register contents and name + + ONE_IF_NOT_DEFAULT(entry, reg.type) + + ONE_IF_NOT_DEFAULT(entry, reg.width) + // Additional entries, if any: + + (size_t) (entry.data.reg.additional_data == NULL + ? 0 + : entry.data.reg.additional_data->dv_hashtab.ht_used)); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR(REG_KEY_CONTENTS); + msgpack_pack_array(spacker, entry.data.reg.contents_size); + for (size_t i = 0; i < entry.data.reg.contents_size; i++) { + PACK_BIN(cstr_as_string(entry.data.reg.contents[i])); + } + PACK_STATIC_STR(KEY_NAME_CHAR); + msgpack_pack_char(spacker, entry.data.reg.name); + if (!CHECK_DEFAULT(entry, reg.type)) { + PACK_STATIC_STR(REG_KEY_TYPE); + msgpack_pack_uint8(spacker, entry.data.reg.type); + } + if (!CHECK_DEFAULT(entry, reg.width)) { + PACK_STATIC_STR(REG_KEY_WIDTH); + msgpack_pack_uint64(spacker, (uint64_t) entry.data.reg.width); + } + DUMP_ADDITIONAL_DATA(entry.data.reg.additional_data); + break; + } + case kSDItemBufferList: { + msgpack_pack_array(spacker, entry.data.buffer_list.size); + for (size_t i = 0; i < entry.data.buffer_list.size; i++) { + const size_t map_size = (size_t) ( + 1 // Buffer name + + (size_t) (entry.data.buffer_list.buffers[i].pos.lnum + != default_pos.lnum) + + (size_t) (entry.data.buffer_list.buffers[i].pos.col + != default_pos.col) + // Additional entries, if any: + + (size_t) ( + entry.data.buffer_list.buffers[i].additional_data == NULL + ? 0 + : (entry.data.buffer_list.buffers[i].additional_data + ->dv_hashtab.ht_used))); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR(KEY_FILE); + PACK_BIN(cstr_as_string(entry.data.buffer_list.buffers[i].fname)); + if (entry.data.buffer_list.buffers[i].pos.lnum != 1) { + PACK_STATIC_STR(KEY_LNUM); + msgpack_pack_uint64( + spacker, (uint64_t) entry.data.buffer_list.buffers[i].pos.lnum); + } + if (entry.data.buffer_list.buffers[i].pos.col != 0) { + PACK_STATIC_STR(KEY_COL); + msgpack_pack_uint64( + spacker, (uint64_t) entry.data.buffer_list.buffers[i].pos.col); + } + DUMP_ADDITIONAL_DATA(entry.data.buffer_list.buffers[i].additional_data); + } + break; + } + case kSDItemHeader: { + msgpack_pack_map(spacker, entry.data.header.size); + for (size_t i = 0; i < entry.data.header.size; i++) { + PACK_STRING(entry.data.header.items[i].key); + const Object obj = entry.data.header.items[i].value; + switch (obj.type) { + case kObjectTypeString: { + PACK_BIN(obj.data.string); + break; + } + case kObjectTypeInteger: { + msgpack_pack_int64(spacker, (int64_t) obj.data.integer); + break; + } + default: { + assert(false); + } + } + } + break; + } + } +#undef CHECK_DEFAULT +#undef ONE_IF_NOT_DEFAULT + if (!max_kbyte || sbuf.size <= max_kbyte * 1024) { + if (entry.type == kSDItemUnknown) { + if (msgpack_pack_uint64(packer, entry.data.unknown_item.type) == -1) { + goto shada_pack_entry_error; + } + } else { + if (msgpack_pack_uint64(packer, (uint64_t) entry.type) == -1) { + goto shada_pack_entry_error; + } + } + if (msgpack_pack_uint64(packer, (uint64_t) entry.timestamp) == -1) { + goto shada_pack_entry_error; + } + if (sbuf.size > 0) { + if ((msgpack_pack_uint64(packer, (uint64_t) sbuf.size) == -1) + || (packer->callback(packer->data, sbuf.data, + (unsigned) sbuf.size) == -1)) { + goto shada_pack_entry_error; + } + } + } + msgpack_packer_free(spacker); + msgpack_sbuffer_destroy(&sbuf); + return true; +shada_pack_entry_error: + msgpack_packer_free(spacker); + msgpack_sbuffer_destroy(&sbuf); + return false; +} +#undef PACK_STRING + +/// Write single ShaDa entry, converting it if needed +/// +/// @warning Frees entry after packing. +/// +/// @param[in] packer Packer used to write entry. +/// @param[in] sd_conv Conversion definitions. +/// @param[in] entry Entry written. If entry.can_free_entry is false then +/// it assumes that entry was not converted, otherwise it +/// is assumed that entry was already converted. +/// @param[in] max_kbyte Maximum size of an item in KiB. Zero means no +/// restrictions. +static bool shada_pack_encoded_entry(msgpack_packer *const packer, + const vimconv_T *const sd_conv, + PossiblyFreedShadaEntry entry, + const size_t max_kbyte) + FUNC_ATTR_NONNULL_ALL +{ + bool ret = true; + if (entry.can_free_entry) { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + shada_free_shada_entry(&entry.data); + return ret; + } +#define RUN_WITH_CONVERTED_STRING(cstr, code) \ + do { \ + bool did_convert = false; \ + if (sd_conv->vc_type != CONV_NONE && has_non_ascii((cstr))) { \ + char *const converted_string = string_convert(sd_conv, (cstr), NULL); \ + if (converted_string != NULL) { \ + (cstr) = converted_string; \ + did_convert = true; \ + } \ + } \ + code \ + if (did_convert) { \ + xfree((cstr)); \ + } \ + } while (0) + switch (entry.data.type) { + case kSDItemUnknown: + case kSDItemMissing: { + assert(false); + } + case kSDItemSearchPattern: { + RUN_WITH_CONVERTED_STRING(entry.data.data.search_pattern.pat, { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + }); + break; + } + case kSDItemHistoryEntry: { + RUN_WITH_CONVERTED_STRING(entry.data.data.history_item.string, { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + }); + break; + } + case kSDItemSubString: { + RUN_WITH_CONVERTED_STRING(entry.data.data.sub_string.sub, { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + }); + break; + } + case kSDItemVariable: { + if (sd_conv->vc_type != CONV_NONE) { + typval_T tgttv; + var_item_copy(sd_conv, &entry.data.data.global_var.value, &tgttv, + true, 0); + clear_tv(&entry.data.data.global_var.value); + entry.data.data.global_var.value = tgttv; + } + ret = shada_pack_entry(packer, entry.data, max_kbyte); + break; + } + case kSDItemRegister: { + bool did_convert = false; + if (sd_conv->vc_type != CONV_NONE) { + size_t first_non_ascii = 0; + for (size_t i = 0; i < entry.data.data.reg.contents_size; i++) { + if (has_non_ascii(entry.data.data.reg.contents[i])) { + first_non_ascii = i; + did_convert = true; + break; + } + } + if (did_convert) { + entry.data.data.reg.contents = + xmemdup(entry.data.data.reg.contents, + (entry.data.data.reg.contents_size + * sizeof(entry.data.data.reg.contents))); + for (size_t i = 0; i < entry.data.data.reg.contents_size; i++) { + if (i >= first_non_ascii) { + entry.data.data.reg.contents[i] = get_converted_string( + sd_conv, + entry.data.data.reg.contents[i], + strlen(entry.data.data.reg.contents[i])); + } else { + entry.data.data.reg.contents[i] = + xstrdup(entry.data.data.reg.contents[i]); + } + } + } + } + ret = shada_pack_entry(packer, entry.data, max_kbyte); + if (did_convert) { + for (size_t i = 0; i < entry.data.data.reg.contents_size; i++) { + xfree(entry.data.data.reg.contents[i]); + } + xfree(entry.data.data.reg.contents); + } + break; + } + case kSDItemHeader: + case kSDItemGlobalMark: + case kSDItemJump: + case kSDItemBufferList: + case kSDItemLocalMark: + case kSDItemChange: { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + break; + } + } +#undef RUN_WITH_CONVERTED_STRING + return ret; +} + +/// Compare two FileMarks structure to order them by greatest_timestamp +/// +/// Order is reversed: structure with greatest greatest_timestamp comes first. +/// Function signature is compatible with qsort. +static int compare_file_marks(const void *a, const void *b) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + const FileMarks *const *const a_fms = a; + const FileMarks *const *const b_fms = b; + return ((*a_fms)->greatest_timestamp == (*b_fms)->greatest_timestamp + ? 0 + : ((*a_fms)->greatest_timestamp > (*b_fms)->greatest_timestamp + ? -1 + : 1)); +} + +/// Parse msgpack object that has given length +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[in] length Object length. +/// @param[out] ret_unpacked Location where read result should be saved. If +/// NULL then unpacked data will be freed. Must be +/// NULL if `ret_buf` is NULL. +/// @param[out] ret_buf Buffer containing parsed string. +/// +/// @return kSDReadStatusNotShaDa, kSDReadStatusReadError or +/// kSDReadStatusSuccess. +static inline ShaDaReadResult shada_parse_msgpack( + ShaDaReadDef *const sd_reader, const size_t length, + msgpack_unpacked *ret_unpacked, char **const ret_buf) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(1) +{ + const uintmax_t initial_fpos = sd_reader->fpos; + char *const buf = xmalloc(length); + + const ShaDaReadResult fl_ret = fread_len(sd_reader, buf, length); + if (fl_ret != kSDReadStatusSuccess) { + xfree(buf); + return fl_ret; + } + bool did_try_to_free = false; +shada_parse_msgpack_read_next: {} + size_t off = 0; + msgpack_unpacked unpacked; + msgpack_unpacked_init(&unpacked); + const msgpack_unpack_return result = + msgpack_unpack_next(&unpacked, buf, length, &off); + ShaDaReadResult ret = kSDReadStatusSuccess; + switch (result) { + case MSGPACK_UNPACK_SUCCESS: { + if (off < length) { + goto shada_parse_msgpack_extra_bytes; + } + break; + } + case MSGPACK_UNPACK_PARSE_ERROR: { + emsgu(_(RCERR "Failed to parse ShaDa file due to a msgpack parser error " + "at position %" PRIu64), + (uint64_t) initial_fpos); + ret = kSDReadStatusNotShaDa; + break; + } + case MSGPACK_UNPACK_NOMEM_ERROR: { + if (!did_try_to_free) { + did_try_to_free = true; + try_to_free_memory(); + goto shada_parse_msgpack_read_next; + } + EMSG(_(e_outofmem)); + ret = kSDReadStatusReadError; + break; + } + case MSGPACK_UNPACK_CONTINUE: { + emsgu(_(RCERR "Failed to parse ShaDa file: incomplete msgpack string " + "at position %" PRIu64), + (uint64_t) initial_fpos); + ret = kSDReadStatusNotShaDa; + break; + } + case MSGPACK_UNPACK_EXTRA_BYTES: { +shada_parse_msgpack_extra_bytes: + emsgu(_(RCERR "Failed to parse ShaDa file: extra bytes in msgpack string " + "at position %" PRIu64), + (uint64_t) initial_fpos); + ret = kSDReadStatusNotShaDa; + break; + } + } + if (ret_buf != NULL && ret == kSDReadStatusSuccess) { + if (ret_unpacked == NULL) { + msgpack_unpacked_destroy(&unpacked); + } else { + *ret_unpacked = unpacked; + } + *ret_buf = buf; + } else { + assert(ret_buf == NULL || ret != kSDReadStatusSuccess); + msgpack_unpacked_destroy(&unpacked); + xfree(buf); + } + return ret; +} + +/// Read and merge in ShaDa file, used when writing +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[in] srni_flags Flags determining what to read. +/// @param[in] max_kbyte Maximum size of one element. +/// @param[in,out] ret_wms Location where results are saved. +/// @param[out] packer MessagePack packer for entries which are not +/// merged. +static inline ShaDaWriteResult shada_read_when_writing( + ShaDaReadDef *const sd_reader, const unsigned srni_flags, + const size_t max_kbyte, WriteMergerState *const wms, + msgpack_packer *const packer) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + ShaDaWriteResult ret = kSDWriteSuccessfull; + ShadaEntry entry; + ShaDaReadResult srni_ret; + while ((srni_ret = shada_read_next_item(sd_reader, &entry, srni_flags, + max_kbyte)) + != kSDReadStatusFinished) { + switch (srni_ret) { + case kSDReadStatusSuccess: { + break; + } + case kSDReadStatusFinished: { + // Should be handled by the while condition. + assert(false); + } + case kSDReadStatusNotShaDa: { + ret = kSDWriteReadNotShada; + // fallthrough + } + case kSDReadStatusReadError: { + return ret; + } + case kSDReadStatusMalformed: { + continue; + } + } +#define COMPARE_WITH_ENTRY(wms_entry_, entry) \ + do { \ + PossiblyFreedShadaEntry *const wms_entry = (wms_entry_); \ + if (wms_entry->data.type != kSDItemMissing) { \ + if (wms_entry->data.timestamp >= (entry).timestamp) { \ + shada_free_shada_entry(&(entry)); \ + break; \ + } \ + if (wms_entry->can_free_entry) { \ + shada_free_shada_entry(&wms_entry->data); \ + } \ + } \ + wms_entry->can_free_entry = true; \ + wms_entry->data = (entry); \ + } while (0) + switch (entry.type) { + case kSDItemMissing: { + break; + } + case kSDItemHeader: + case kSDItemBufferList: { + assert(false); + } + case kSDItemUnknown: { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + shada_free_shada_entry(&entry); + break; + } + case kSDItemSearchPattern: { + COMPARE_WITH_ENTRY((entry.data.search_pattern.is_substitute_pattern + ? &wms->sub_search_pattern + : &wms->search_pattern), entry); + break; + } + case kSDItemSubString: { + COMPARE_WITH_ENTRY(&wms->replacement, entry); + break; + } + case kSDItemHistoryEntry: { + if (entry.data.history_item.histtype >= HIST_COUNT) { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + shada_free_shada_entry(&entry); + break; + } + hms_insert(&wms->hms[entry.data.history_item.histtype], entry, true, + true); + break; + } + case kSDItemRegister: { + const int idx = op_reg_index(entry.data.reg.name); + if (idx < 0) { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + shada_free_shada_entry(&entry); + break; + } + COMPARE_WITH_ENTRY(&wms->registers[idx], entry); + break; + } + case kSDItemVariable: { + if (!in_strset(&wms->dumped_variables, entry.data.global_var.name)) { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + } + shada_free_shada_entry(&entry); + break; + } + case kSDItemGlobalMark: { + const int idx = mark_global_index(entry.data.filemark.name); + if (idx < 0) { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + shada_free_shada_entry(&entry); + break; + } + COMPARE_WITH_ENTRY(&wms->global_marks[idx], entry); + break; + } + case kSDItemChange: + case kSDItemLocalMark: { + if (shada_removable(entry.data.filemark.fname)) { + shada_free_shada_entry(&entry); + break; + } + const char *const fname = (const char *) entry.data.filemark.fname; + khiter_t k; + int kh_ret; + k = kh_put(file_marks, &wms->file_marks, fname, &kh_ret); + FileMarks *const filemarks = &kh_val(&wms->file_marks, k); + if (kh_ret > 0) { + memset(filemarks, 0, sizeof(*filemarks)); + } + if (entry.timestamp > filemarks->greatest_timestamp) { + filemarks->greatest_timestamp = entry.timestamp; + } + if (entry.type == kSDItemLocalMark) { + const int idx = mark_local_index(entry.data.filemark.name); + if (idx < 0) { + filemarks->additional_marks = xrealloc( + filemarks->additional_marks, + (++filemarks->additional_marks_size + * sizeof(filemarks->additional_marks[0]))); + filemarks->additional_marks[filemarks->additional_marks_size - 1] = + entry; + } else { + PossiblyFreedShadaEntry *const wms_entry = &filemarks->marks[idx]; + if (wms_entry->data.type != kSDItemMissing) { + if (wms_entry->data.timestamp >= entry.timestamp) { + shada_free_shada_entry(&entry); + break; + } + if (wms_entry->can_free_entry) { + if (kh_key(&wms->file_marks, k) + == wms_entry->data.data.filemark.fname) { + kh_key(&wms->file_marks, k) = entry.data.filemark.fname; + } + shada_free_shada_entry(&wms_entry->data); + } + } + wms_entry->can_free_entry = true; + wms_entry->data = entry; + } + } else { +#define FREE_POSSIBLY_FREED_SHADA_ENTRY(entry) \ + do { \ + if (entry.can_free_entry) { \ + shada_free_shada_entry(&entry.data); \ + } \ + } while (0) +#define SDE_TO_PFSDE(entry) \ + ((PossiblyFreedShadaEntry) { .can_free_entry = true, .data = entry }) +#define AFTERFREE_DUMMY(entry) +#define DUMMY_IDX_ADJ(i) + MERGE_JUMPS(filemarks->changes_size, filemarks->changes, + PossiblyFreedShadaEntry, data.timestamp, + data.data.filemark.mark, entry, true, + FREE_POSSIBLY_FREED_SHADA_ENTRY, SDE_TO_PFSDE, + DUMMY_IDX_ADJ, AFTERFREE_DUMMY); + } + break; + } + case kSDItemJump: { + MERGE_JUMPS(wms->jumps_size, wms->jumps, PossiblyFreedShadaEntry, + data.timestamp, data.data.filemark.mark, entry, + strcmp(jl_entry.data.data.filemark.fname, + entry.data.filemark.fname) == 0, + FREE_POSSIBLY_FREED_SHADA_ENTRY, SDE_TO_PFSDE, + DUMMY_IDX_ADJ, AFTERFREE_DUMMY); +#undef FREE_POSSIBLY_FREED_SHADA_ENTRY +#undef SDE_TO_PFSDE +#undef DUMMY_IDX_ADJ +#undef AFTERFREE_DUMMY + break; + } + } + } +#undef COMPARE_WITH_ENTRY + return ret; +} + +/// Write ShaDa file +/// +/// @param[in] sd_writer Structure containing file writer definition. +/// @param[in] sd_reader Structure containing file reader definition. If it is +/// not NULL then contents of this file will be merged +/// with current Neovim runtime. +static ShaDaWriteResult shada_write(ShaDaWriteDef *const sd_writer, + ShaDaReadDef *const sd_reader) + FUNC_ATTR_NONNULL_ARG(1) +{ + ShaDaWriteResult ret = kSDWriteSuccessfull; + int max_kbyte_i = get_shada_parameter('s'); + if (max_kbyte_i < 0) { + max_kbyte_i = 10; + } + if (max_kbyte_i == 0) { + return ret; + } + + WriteMergerState *const wms = xcalloc(1, sizeof(*wms)); + bool dump_one_history[HIST_COUNT]; + const bool dump_global_vars = (find_shada_parameter('!') != NULL); + int max_reg_lines = get_shada_parameter('<'); + if (max_reg_lines < 0) { + max_reg_lines = get_shada_parameter('"'); + } + const bool limit_reg_lines = max_reg_lines >= 0; + const bool dump_registers = (max_reg_lines != 0); + khash_t(bufset) removable_bufs = KHASH_EMPTY_TABLE(bufset); + const size_t max_kbyte = (size_t) max_kbyte_i; + const size_t num_marked_files = (size_t) get_shada_parameter('\''); + const bool dump_global_marks = get_shada_parameter('f') != 0; + bool dump_history = false; + + // Initialize history merger + for (uint8_t i = 0; i < HIST_COUNT; i++) { + long num_saved = get_shada_parameter(hist_type2char(i)); + if (num_saved == -1) { + num_saved = p_hi; + } + if (num_saved > 0) { + dump_history = true; + dump_one_history[i] = true; + hms_init(&wms->hms[i], i, (size_t) num_saved, sd_reader != NULL, false); + } else { + dump_one_history[i] = false; + } + } + + const unsigned srni_flags = (unsigned) ( + kSDReadUndisableableData + | kSDReadUnknown + | (dump_history ? kSDReadHistory : 0) + | (dump_registers ? kSDReadRegisters : 0) + | (dump_global_vars ? kSDReadVariables : 0) + | (dump_global_marks ? kSDReadGlobalMarks : 0) + | (num_marked_files ? kSDReadLocalMarks | kSDReadChanges : 0)); + + msgpack_packer *const packer = msgpack_packer_new(sd_writer, + &msgpack_sd_writer_write); + + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL && shada_removable((char *) buf->b_ffname)) { + int kh_ret; + (void) kh_put(bufset, &removable_bufs, (uintptr_t) buf, &kh_ret); + } + } + + // Write header + if (!shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemHeader, + .timestamp = os_time(), + .data = { + .header = { + .size = 5, + .capacity = 5, + .items = ((KeyValuePair[]) { + { STATIC_CSTR_AS_STRING("generator"), + STRING_OBJ(STATIC_CSTR_AS_STRING("nvim")) }, + { 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()) }, + { STATIC_CSTR_AS_STRING("encoding"), + STRING_OBJ(cstr_as_string((char *) p_enc)) }, + }), + } + } + }, 0)) { + ret = kSDWriteFailed; + goto shada_write_exit; + } + + // Write buffer list + if (find_shada_parameter('%') != NULL) { + size_t buf_count = 0; + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL && !in_bufset(&removable_bufs, buf)) { + buf_count++; + } + } + + ShadaEntry buflist_entry = (ShadaEntry) { + .type = kSDItemBufferList, + .timestamp = os_time(), + .data = { + .buffer_list = { + .size = buf_count, + .buffers = xmalloc(buf_count + * sizeof(*buflist_entry.data.buffer_list.buffers)), + }, + }, + }; + size_t i = 0; + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname == NULL || in_bufset(&removable_bufs, buf)) { + continue; + } + buflist_entry.data.buffer_list.buffers[i] = (struct buffer_list_buffer) { + .pos = buf->b_last_cursor.mark, + .fname = (char *) buf->b_ffname, + .additional_data = buf->additional_data, + }; + i++; + } + if (!shada_pack_entry(packer, buflist_entry, 0)) { + xfree(buflist_entry.data.buffer_list.buffers); + ret = kSDWriteFailed; + goto shada_write_exit; + } + xfree(buflist_entry.data.buffer_list.buffers); + } + + // Write some of the variables + if (dump_global_vars) { + const void *var_iter = NULL; + const Timestamp cur_timestamp = os_time(); + do { + typval_T vartv; + const char *name = NULL; + var_iter = var_shada_iter(var_iter, &name, &vartv); + if (name == NULL) { + break; + } + typval_T tgttv; + if (sd_writer->sd_conv.vc_type != CONV_NONE) { + var_item_copy(&sd_writer->sd_conv, &vartv, &tgttv, true, 0); + } else { + copy_tv(&vartv, &tgttv); + } + if (!shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemVariable, + .timestamp = cur_timestamp, + .data = { + .global_var = { + .name = (char *) name, + .value = tgttv, + .additional_elements = NULL, + } + } + }, max_kbyte)) { + clear_tv(&vartv); + clear_tv(&tgttv); + ret = kSDWriteFailed; + goto shada_write_exit; + } + clear_tv(&vartv); + clear_tv(&tgttv); + int kh_ret; + (void) kh_put(strset, &wms->dumped_variables, name, &kh_ret); + } while (var_iter != NULL); + } + + const bool search_highlighted = !(no_hlsearch + || find_shada_parameter('h') != NULL); + const bool search_last_used = search_was_last_used(); +#define ADD_SEARCH_PAT(func, wms_attr, hlo, pcae, o, is_sub) \ + do { \ + SearchPattern pat; \ + func(&pat); \ + if (pat.pat != NULL) { \ + wms->wms_attr = (PossiblyFreedShadaEntry) { \ + .can_free_entry = false, \ + .data = { \ + .type = kSDItemSearchPattern, \ + .timestamp = pat.timestamp, \ + .data = { \ + .search_pattern = { \ + .magic = pat.magic, \ + .smartcase = !pat.no_scs, \ + .has_line_offset = hlo, \ + .place_cursor_at_end = pcae, \ + .offset = o, \ + .is_last_used = (is_sub ^ search_last_used), \ + .is_substitute_pattern = is_sub, \ + .highlighted = ((is_sub ^ search_last_used) \ + && search_highlighted), \ + .pat = (char *) pat.pat, \ + .additional_data = pat.additional_data, \ + } \ + } \ + } \ + }; \ + } \ + } while (0) + + // Initialize search pattern + ADD_SEARCH_PAT(get_search_pattern, search_pattern, pat.off.line, \ + pat.off.end, pat.off.off, false); + + // Initialize substitute search pattern + ADD_SEARCH_PAT(get_substitute_pattern, sub_search_pattern, false, false, 0, + true); +#undef ADD_SEARCH_PAT + + // Initialize substitute replacement string + { + SubReplacementString sub; + sub_get_replacement(&sub); + wms->replacement = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemSubString, + .timestamp = sub.timestamp, + .data = { + .sub_string = { + .sub = (char *) sub.sub, + .additional_elements = sub.additional_elements, + } + } + } + }; + } + + // Initialize jump list + const void *jump_iter = NULL; + do { + xfmark_T fm; + cleanup_jumplist(); + jump_iter = mark_jumplist_iter(jump_iter, curwin, &fm); + const buf_T *const buf = (fm.fmark.fnum == 0 + ? NULL + : buflist_findnr(fm.fmark.fnum)); + if (buf != NULL + ? in_bufset(&removable_bufs, buf) + : fm.fmark.fnum != 0) { + continue; + } + const char *const fname = (char *) (fm.fmark.fnum == 0 + ? (fm.fname == NULL ? NULL : fm.fname) + : buf->b_ffname); + if (fname == NULL) { + continue; + } + wms->jumps[wms->jumps_size++] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemJump, + .timestamp = fm.fmark.timestamp, + .data = { + .filemark = { + .name = NUL, + .mark = fm.fmark.mark, + .fname = (char *) fname, + .additional_data = fm.fmark.additional_data, + } + } + } + }; + } while (jump_iter != NULL); + + // Initialize global marks + if (dump_global_marks) { + const void *global_mark_iter = NULL; + do { + char name = NUL; + xfmark_T fm; + global_mark_iter = mark_global_iter(global_mark_iter, &name, &fm); + if (name == NUL) { + break; + } + const char *fname; + if (fm.fmark.fnum == 0) { + assert(fm.fname != NULL); + if (shada_removable((const char *) fm.fname)) { + continue; + } + fname = (const char *) fm.fname; + } else { + const buf_T *const buf = buflist_findnr(fm.fmark.fnum); + if (buf == NULL || buf->b_ffname == NULL + || in_bufset(&removable_bufs, buf)) { + continue; + } + fname = (const char *) buf->b_ffname; + } + wms->global_marks[mark_global_index(name)] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemGlobalMark, + .timestamp = fm.fmark.timestamp, + .data = { + .filemark = { + .mark = fm.fmark.mark, + .name = name, + .additional_data = fm.fmark.additional_data, + .fname = (char *) fname, + } + } + }, + }; + } while (global_mark_iter != NULL); + } + + // Initialize registers + if (dump_registers) { + const void *reg_iter = NULL; + do { + yankreg_T reg; + char name = NUL; + reg_iter = op_register_iter(reg_iter, &name, ®); + if (name == NUL) { + break; + } + if (limit_reg_lines && reg.y_size > max_reg_lines) { + continue; + } + wms->registers[op_reg_index(name)] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemRegister, + .timestamp = reg.timestamp, + .data = { + .reg = { + .contents = (char **) reg.y_array, + .contents_size = (size_t) reg.y_size, + .type = (uint8_t) reg.y_type, + .width = (size_t) (reg.y_type == MBLOCK ? reg.y_width : 0), + .additional_data = reg.additional_data, + .name = name, + } + } + } + }; + } while (reg_iter != NULL); + } + + // Initialize buffers + if (num_marked_files > 0) { + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname == NULL || in_bufset(&removable_bufs, buf)) { + continue; + } + const void *local_marks_iter = NULL; + const char *const fname = (const char *) buf->b_ffname; + khiter_t k; + int kh_ret; + k = kh_put(file_marks, &wms->file_marks, fname, &kh_ret); + FileMarks *const filemarks = &kh_val(&wms->file_marks, k); + if (kh_ret > 0) { + memset(filemarks, 0, sizeof(*filemarks)); + } + do { + fmark_T fm; + char name = NUL; + local_marks_iter = mark_buffer_iter(local_marks_iter, buf, &name, &fm); + if (name == NUL) { + break; + } + filemarks->marks[mark_local_index(name)] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemLocalMark, + .timestamp = fm.timestamp, + .data = { + .filemark = { + .mark = fm.mark, + .name = name, + .fname = (char *) fname, + .additional_data = fm.additional_data, + } + } + } + }; + if (fm.timestamp > filemarks->greatest_timestamp) { + filemarks->greatest_timestamp = fm.timestamp; + } + } while (local_marks_iter != NULL); + for (int i = 0; i < buf->b_changelistlen; i++) { + const fmark_T fm = buf->b_changelist[i]; + filemarks->changes[i] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemChange, + .timestamp = fm.timestamp, + .data = { + .filemark = { + .mark = fm.mark, + .fname = (char *) fname, + .additional_data = fm.additional_data, + } + } + } + }; + if (fm.timestamp > filemarks->greatest_timestamp) { + filemarks->greatest_timestamp = fm.timestamp; + } + } + filemarks->changes_size = (size_t) buf->b_changelistlen; + } + } + + if (sd_reader != NULL) { + const ShaDaWriteResult srww_ret = shada_read_when_writing( + sd_reader, srni_flags, max_kbyte, wms, packer); + if (srww_ret != kSDWriteSuccessfull) { + ret = srww_ret; + } + } + + // Write the rest +#define PACK_WMS_ARRAY(wms_array) \ + do { \ + for (size_t i_ = 0; i_ < ARRAY_SIZE(wms_array); i_++) { \ + if (wms_array[i_].data.type != kSDItemMissing) { \ + if (!shada_pack_encoded_entry(packer, &sd_writer->sd_conv, \ + wms_array[i_], \ + max_kbyte)) { \ + ret = kSDWriteFailed; \ + goto shada_write_exit; \ + } \ + } \ + } \ + } while (0) + PACK_WMS_ARRAY(wms->global_marks); + PACK_WMS_ARRAY(wms->registers); + for (size_t i = 0; i < wms->jumps_size; i++) { + if (!shada_pack_encoded_entry(packer, &sd_writer->sd_conv, wms->jumps[i], + max_kbyte)) { + ret = kSDWriteFailed; + goto shada_write_exit; + } + } +#define PACK_WMS_ENTRY(wms_entry) \ + do { \ + if (wms_entry.data.type != kSDItemMissing) { \ + if (!shada_pack_encoded_entry(packer, &sd_writer->sd_conv, wms_entry, \ + max_kbyte)) { \ + ret = kSDWriteFailed; \ + goto shada_write_exit; \ + } \ + } \ + } while (0) + PACK_WMS_ENTRY(wms->search_pattern); + PACK_WMS_ENTRY(wms->sub_search_pattern); + PACK_WMS_ENTRY(wms->replacement); +#undef PACK_WMS_ENTRY + + const size_t file_markss_size = kh_size(&wms->file_marks); + FileMarks **const all_file_markss = + xmalloc(file_markss_size * sizeof(*all_file_markss)); + FileMarks **cur_file_marks = all_file_markss; + for (khint_t i = kh_begin(&wms->file_marks); i != kh_end(&wms->file_marks); + i++) { + if (kh_exist(&wms->file_marks, i)) { + *cur_file_marks++ = &kh_val(&wms->file_marks, i); + } + } + qsort((void *) all_file_markss, file_markss_size, sizeof(*all_file_markss), + &compare_file_marks); + const size_t file_markss_to_dump = MIN(num_marked_files, file_markss_size); + for (size_t i = 0; i < file_markss_to_dump; i++) { + PACK_WMS_ARRAY(all_file_markss[i]->marks); + for (size_t j = 0; j < all_file_markss[i]->changes_size; j++) { + if (!shada_pack_encoded_entry(packer, &sd_writer->sd_conv, + all_file_markss[i]->changes[j], + max_kbyte)) { + ret = kSDWriteFailed; + goto shada_write_exit; + } + } + for (size_t j = 0; j < all_file_markss[i]->additional_marks_size; j++) { + if (!shada_pack_entry(packer, all_file_markss[i]->additional_marks[j], + 0)) { + shada_free_shada_entry(&all_file_markss[i]->additional_marks[j]); + ret = kSDWriteFailed; + goto shada_write_exit; + } + shada_free_shada_entry(&all_file_markss[i]->additional_marks[j]); + } + xfree(all_file_markss[i]->additional_marks); + } + xfree(all_file_markss); +#undef PACK_WMS_ARRAY + + if (dump_history) { + for (size_t i = 0; i < HIST_COUNT; i++) { + if (dump_one_history[i]) { + hms_insert_whole_neovim_history(&wms->hms[i]); + HMS_ITER(&wms->hms[i], cur_entry) { + if (!shada_pack_encoded_entry( + packer, &sd_writer->sd_conv, (PossiblyFreedShadaEntry) { + .data = cur_entry->data, + .can_free_entry = cur_entry->can_free_entry, + }, max_kbyte)) { + ret = kSDWriteFailed; + break; + } + } + hms_dealloc(&wms->hms[i]); + if (ret == kSDWriteFailed) { + goto shada_write_exit; + } + } + } + } + +shada_write_exit: + kh_dealloc(file_marks, &wms->file_marks); + kh_dealloc(bufset, &removable_bufs); + msgpack_packer_free(packer); + kh_dealloc(strset, &wms->dumped_variables); + xfree(wms); + return ret; +} + +#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, bool nomerge) +{ + if (shada_disabled()) { + return FAIL; + } + + char *const fname = shada_filename(file); + char *tempname = NULL; + ShaDaWriteDef sd_writer = (ShaDaWriteDef) { + .write = &write_file, + .close = &close_sd_writer, + .error = NULL, + }; + ShaDaReadDef sd_reader; + + intptr_t fd; + + if (!nomerge) { + if (open_shada_file_for_reading(fname, &sd_reader) != 0) { + nomerge = true; + goto shada_write_file_nomerge; + } + tempname = modname(fname, ".tmp.a", false); + if (tempname == NULL) { + nomerge = true; + goto shada_write_file_nomerge; + } + + // Save permissions from the original file, with modifications: + int perm = (int) os_getperm(fname); + perm = (perm >= 0) ? ((perm & 0777) | 0600) : 0600; + // ^3 ^1 ^2 ^2,3 + // 1: Strip SUID bit if any. + // 2: Make sure that user can always read and write the result. + // 3: If somebody happened to delete the file after it was opened for + // reading use u=rw permissions. +shada_write_file_open: + fd = (intptr_t) open_file(tempname, O_CREAT|O_WRONLY|O_NOFOLLOW|O_EXCL, + perm); + if (fd < 0) { + if (-fd == EEXIST +#ifdef ELOOP + || -fd == ELOOP +#endif + ) { + // File already exists, try another name + char *const wp = tempname + strlen(tempname) - 1; + if (*wp == 'z') { + // Tried names from .tmp.a to .tmp.z, all failed. Something must be + // wrong then. + EMSG2(_("E138: All %s.tmp.X files exist, cannot write ShaDa file!"), + fname); + xfree(fname); + xfree(tempname); + return FAIL; + } else { + (*wp)++; + goto shada_write_file_open; + } + } + } + } + if (nomerge) { +shada_write_file_nomerge: {} + char *const tail = path_tail_with_sep(fname); + if (tail != fname) { + const char tail_save = *tail; + *tail = NUL; + if (!os_isdir(fname)) { + int ret; + char *failed_dir; + if ((ret = os_mkdir_recurse(fname, 0700, &failed_dir)) != 0) { + EMSG3(_(SERR "Failed to create directory %s " + "for writing ShaDa file: %s"), + failed_dir, os_strerror(ret)); + xfree(fname); + xfree(failed_dir); + return FAIL; + } + } + *tail = tail_save; + } + fd = (intptr_t) open_file(fname, O_CREAT|O_WRONLY|O_TRUNC, + 0600); + } + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Writing ShaDa file \"%s\""), fname); + verbose_leave(); + } + + if (fd < 0) { + xfree(fname); + xfree(tempname); + return FAIL; + } + + sd_writer.cookie = (void *) fd; + + convert_setup(&sd_writer.sd_conv, p_enc, "utf-8"); + + const ShaDaWriteResult sw_ret = shada_write(&sd_writer, (nomerge + ? NULL + : &sd_reader)); +#ifndef UNIX + sd_writer.close(&sd_writer); +#endif + if (!nomerge) { + sd_reader.close(&sd_reader); + bool did_remove = false; + if (sw_ret == kSDWriteSuccessfull) { +#ifdef UNIX + bool closed = false; + // 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; + if (os_fileinfo((char *)fname, &old_info)) { + if (getuid() == ROOT_UID) { + if (old_info.stat.st_uid != ROOT_UID + || old_info.stat.st_gid != getgid()) { + const uv_uid_t old_uid = (uv_uid_t) old_info.stat.st_uid; + const uv_gid_t old_gid = (uv_gid_t) old_info.stat.st_gid; + const int fchown_ret = os_fchown((int) fd, old_uid, old_gid); + sd_writer.close(&sd_writer); + if (fchown_ret != 0) { + EMSG3(_(RNERR "Failed setting uid and gid for file %s: %s"), + tempname, os_strerror(fchown_ret)); + goto shada_write_file_did_not_remove; + } + closed = true; + } + } else if (!(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)))) { + EMSG2(_("E137: ShaDa file is not writable: %s"), fname); + sd_writer.close(&sd_writer); + goto shada_write_file_did_not_remove; + } + } + if (!closed) { + sd_writer.close(&sd_writer); + } +#endif + if (vim_rename(tempname, fname) == -1) { + EMSG3(_(RNERR "Can't rename ShaDa file from %s to %s!"), + tempname, fname); + } else { + did_remove = true; + os_remove(tempname); + } + } else { + if (sw_ret == kSDWriteReadNotShada) { + EMSG3(_(RNERR "Did not rename %s because %s " + "does not looks like a ShaDa file"), tempname, fname); + } else { + EMSG3(_(RNERR "Did not rename %s to %s because there were errors " + "during writing it"), tempname, fname); + } + } + if (!did_remove) { +#ifdef UNIX +shada_write_file_did_not_remove: +#endif + EMSG3(_(RNERR "Do not forget to remove %s or rename it manually to %s."), + tempname, fname); + } + xfree(tempname); + } + + xfree(fname); + 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 +/// @param[in] forceit If true, use forced reading (prioritize file contents +/// over current Neovim state). +/// @param[in] missing_ok If true, do not error out when file is missing. +/// +/// @return OK in case of success, FAIL otherwise. +int shada_read_everything(const char *const fname, const bool forceit, + const bool missing_ok) +{ + return shada_read_file(fname, + kShaDaWantInfo|kShaDaWantMarks|kShaDaGetOldfiles + |(forceit?kShaDaForceit:0) + |(missing_ok?0:kShaDaMissingError)); +} + +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 kSDItemChange: + case kSDItemJump: + case kSDItemGlobalMark: + case kSDItemLocalMark: { + dict_unref(entry->data.filemark.additional_data); + xfree(entry->data.filemark.fname); + break; + } + case kSDItemSearchPattern: { + dict_unref(entry->data.search_pattern.additional_data); + xfree(entry->data.search_pattern.pat); + break; + } + case kSDItemRegister: { + dict_unref(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: { + list_unref(entry->data.history_item.additional_elements); + xfree(entry->data.history_item.string); + break; + } + case kSDItemVariable: { + list_unref(entry->data.global_var.additional_elements); + xfree(entry->data.global_var.name); + clear_tv(&entry->data.global_var.value); + break; + } + case kSDItemSubString: { + list_unref(entry->data.sub_string.additional_elements); + xfree(entry->data.sub_string.sub); + break; + } + case kSDItemBufferList: { + for (size_t i = 0; i < entry->data.buffer_list.size; i++) { + xfree(entry->data.buffer_list.buffers[i].fname); + dict_unref(entry->data.buffer_list.buffers[i].additional_data); + } + xfree(entry->data.buffer_list.buffers); + break; + } + } +} + +#ifndef HAVE_BE64TOH +static inline uint64_t be64toh(uint64_t big_endian_64_bits) +{ +#ifdef ORDER_BIG_ENDIAN + return big_endian_64_bits; +#else + // It may appear that when !defined(ORDER_BIG_ENDIAN) actual order is big + // endian. This variant is suboptimal, but it works regardless of actual + // order. + uint8_t *buf = (uint8_t *) &big_endian_64_bits; + uint64_t ret = 0; + for (size_t i = 8; i; i--) { + ret |= ((uint64_t) buf[i - 1]) << ((8 - i) * 8); + } + return ret; +#endif +} +#endif + +/// Read given number of bytes into given buffer, display error if needed +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[out] buffer Where to save the results. +/// @param[in] length How many bytes should be read. +/// +/// @return kSDReadStatusSuccess if everything was OK, kSDReadStatusNotShaDa if +/// there were not enough bytes to read or kSDReadStatusReadError if +/// there was some error while reading. +static ShaDaReadResult fread_len(ShaDaReadDef *const sd_reader, + char *const buffer, + const size_t length) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + const ptrdiff_t read_bytes = sd_reader->read(sd_reader, buffer, length); + (void) read_bytes; + + if (sd_reader->error != NULL) { + emsg2(_(SERR "System error while reading ShaDa file: %s"), + sd_reader->error); + return kSDReadStatusReadError; + } else if (sd_reader->eof) { + emsgu(_(RCERR "Error while reading ShaDa file: " + "last entry specified that it occupies %" PRIu64 " bytes, " + "but file ended earlier"), + (uint64_t) length); + return kSDReadStatusNotShaDa; + } + assert(read_bytes >= 0 && (size_t) read_bytes == length); + return kSDReadStatusSuccess; +} + +/// 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] sd_reader Structure containing file reader definition. +/// @param[out] result Location where result is saved. +/// +/// @return kSDReadStatusSuccess if reading was successfull, +/// kSDReadStatusNotShaDa if there were not enough bytes to read or +/// kSDReadStatusReadError if reading failed for whatever reason. +static ShaDaReadResult msgpack_read_uint64(ShaDaReadDef *const sd_reader, + const int first_char, + uint64_t *const result) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + const uintmax_t fpos = sd_reader->fpos - 1; + + if (first_char == EOF) { + if (sd_reader->error) { + emsg2(_(SERR "System error while reading integer from ShaDa file: %s"), + sd_reader->error); + return kSDReadStatusReadError; + } else if (sd_reader->eof) { + emsgu(_(RCERR "Error while reading ShaDa file: " + "expected positive integer at position %" PRIu64 + ", but got nothing"), + (uint64_t) fpos); + return kSDReadStatusNotShaDa; + } + } + + 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: { + emsgu(_(RCERR "Error while reading ShaDa file: " + "expected positive integer at position %" PRIu64), + (uint64_t) fpos); + return kSDReadStatusNotShaDa; + } + } + uint64_t buf = 0; + char *buf_u8 = (char *) &buf; + ShaDaReadResult fl_ret; + if ((fl_ret = fread_len(sd_reader, &(buf_u8[sizeof(buf)-length]), length)) + != kSDReadStatusSuccess) { + return fl_ret; + } + *result = be64toh(buf); + } + return kSDReadStatusSuccess; +} + +/// Convert or copy and return a string +/// +/// @param[in] sd_conv Conversion definition. +/// @param[in] str String to convert. +/// @param[in] len String length. +/// +/// @return [allocated] converted string or copy of the original string. +static inline char *get_converted_string(const vimconv_T *const sd_conv, + const char *const str, + const size_t len) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (!has_non_ascii_len(str, len)) { + return xmemdupz(str, len); + } + size_t new_len = len; + char *const new_str = string_convert(sd_conv, str, &new_len); + if (new_str == NULL) { + return xmemdupz(str, len); + } + return new_str; +} + +#define READERR(entry_name, error_desc) \ + RERR "Error while reading ShaDa file: " \ + entry_name " entry at position %" PRIu64 " " \ + error_desc +#define CHECK_KEY(key, expected) ( \ + key.via.str.size == sizeof(expected) - 1 \ + && STRNCMP(key.via.str.ptr, expected, sizeof(expected) - 1) == 0) +#define CLEAR_GA_AND_ERROR_OUT(ga) \ + do { \ + ga_clear(&ga); \ + goto shada_read_next_item_error; \ + } while (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)) { \ + emsgu(_(READERR(entry_name, error_desc)), initial_fpos); \ + CLEAR_GA_AND_ERROR_OUT(ad_ga); \ + } \ + 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) { \ + emsgu(_(READERR(entry_name, "has key which is not a string")), \ + initial_fpos); \ + CLEAR_GA_AND_ERROR_OUT(ad_ga); \ + } else if (unpacked.data.via.map.ptr[i].key.via.str.size == 0) { \ + emsgu(_(READERR(entry_name, "has empty key")), initial_fpos); \ + CLEAR_GA_AND_ERROR_OUT(ad_ga); \ + } \ + } 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 CONVERTED_STRING_KEY(entry_name, name, tgt) \ + TYPED_KEY(entry_name, name, "a binary", tgt, BIN, bin, BIN_CONVERTED) +#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++; \ + } +#define CONVERTED(str, len) ( \ + sd_reader->sd_conv.vc_type != CONV_NONE \ + ? get_converted_string(&sd_reader->sd_conv, (str), (len)) \ + : xmemdupz((str), (len))) +#define BIN_CONVERTED(b) CONVERTED(b.ptr, b.size) +#define SET_ADDITIONAL_DATA(tgt, name) \ + do { \ + 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, \ + } \ + } \ + }; \ + typval_T adtv; \ + if (msgpack_to_vim(obj, &adtv) == FAIL \ + || adtv.v_type != VAR_DICT) { \ + emsgu(_(READERR(name, \ + "cannot be converted to a VimL dictionary")), \ + initial_fpos); \ + ga_clear(&ad_ga); \ + clear_tv(&adtv); \ + goto shada_read_next_item_error; \ + } \ + tgt = adtv.vval.v_dict; \ + } \ + ga_clear(&ad_ga); \ + } while (0) +#define SET_ADDITIONAL_ELEMENTS(src, src_maxsize, tgt, name) \ + do { \ + if ((src).size > (size_t) (src_maxsize)) { \ + msgpack_object obj = { \ + .type = MSGPACK_OBJECT_ARRAY, \ + .via = { \ + .array = { \ + .size = ((src).size - (uint32_t) (src_maxsize)), \ + .ptr = (src).ptr + (src_maxsize), \ + } \ + } \ + }; \ + typval_T aetv; \ + if (msgpack_to_vim(obj, &aetv) == FAIL) { \ + emsgu(_(READERR(name, "cannot be converted to a VimL list")), \ + initial_fpos); \ + clear_tv(&aetv); \ + goto shada_read_next_item_error; \ + } \ + assert(aetv.v_type == VAR_LIST); \ + (tgt) = aetv.vval.v_list; \ + } \ + } while (0) + +/// Iterate over shada file contents +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[out] entry Address where next entry contents will be saved. +/// @param[in] flags Flags, determining whether and which items should be +/// skipped (see SRNIFlags enum). +/// @param[in] max_kbyte If non-zero, skip reading entries which have length +/// greater then given. +/// +/// @return Any value from ShaDaReadResult enum. +static ShaDaReadResult shada_read_next_item(ShaDaReadDef *const sd_reader, + ShadaEntry *const entry, + const unsigned flags, + const size_t max_kbyte) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + ShaDaReadResult ret = kSDReadStatusMalformed; +shada_read_next_item_start: + // Set entry type to kSDItemMissing and also make sure that all pointers in + // data union are NULL so they are safe to xfree(). This is needed in case + // somebody calls goto shada_read_next_item_error before anything is set in + // the switch. + memset(entry, 0, sizeof(*entry)); + if (sd_reader->eof) { + return kSDReadStatusFinished; + } + + // 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) kSDItemMissing; + uint64_t timestamp_u64; + uint64_t length_u64; + + const uint64_t initial_fpos = (uint64_t) sd_reader->fpos; + const int first_char = read_char(sd_reader); + if (first_char == EOF && sd_reader->eof) { + return kSDReadStatusFinished; + } + + ShaDaReadResult mru_ret; + if (((mru_ret = msgpack_read_uint64(sd_reader, first_char, &type_u64)) + != kSDReadStatusSuccess) + || ((mru_ret = msgpack_read_uint64(sd_reader, read_char(sd_reader), + ×tamp_u64)) + != kSDReadStatusSuccess) + || ((mru_ret = msgpack_read_uint64(sd_reader, read_char(sd_reader), + &length_u64)) + != kSDReadStatusSuccess)) { + return mru_ret; + } + + const size_t length = (size_t) length_u64; + entry->timestamp = (Timestamp) timestamp_u64; + + if (type_u64 == 0) { + // kSDItemUnknown cannot possibly pass that far because it is -1 and that + // will fail in msgpack_read_uint64. But kSDItemMissing may and it will + // otherwise be skipped because (1 << 0) will never appear in flags. + emsgu(_(RCERR "Error while reading ShaDa file: " + "there is an item at position %" PRIu64 " " + "that must not be there: Missing items are " + "for internal uses only"), + initial_fpos); + return kSDReadStatusNotShaDa; + } + + if ((type_u64 > SHADA_LAST_ENTRY + ? !(flags & kSDReadUnknown) + : !((unsigned) (1 << type_u64) & flags)) + || (max_kbyte && length > max_kbyte * 1024)) { + // First entry is unknown or equal to "\n" (10)? Most likely this means that + // current file is not a ShaDa file because first item should normally be + // a header (excluding tests where first item is tested item). Check this by + // parsing entry contents: in non-ShaDa files this will most likely result + // in incomplete MessagePack string. + if (initial_fpos == 0 + && (type_u64 == '\n' || type_u64 > SHADA_LAST_ENTRY)) { + const ShaDaReadResult spm_ret = shada_parse_msgpack(sd_reader, length, + NULL, NULL); + if (spm_ret != kSDReadStatusSuccess) { + return spm_ret; + } + } else { + const ShaDaReadResult srs_ret = sd_reader_skip(sd_reader, length); + if (srs_ret != kSDReadStatusSuccess) { + return srs_ret; + } + } + goto shada_read_next_item_start; + } + + if (type_u64 > SHADA_LAST_ENTRY) { + entry->type = kSDItemUnknown; + entry->data.unknown_item.size = length; + entry->data.unknown_item.type = type_u64; + if (initial_fpos == 0) { + const ShaDaReadResult spm_ret = shada_parse_msgpack( + sd_reader, length, NULL, &entry->data.unknown_item.contents); + if (spm_ret != kSDReadStatusSuccess) { + entry->type = kSDItemMissing; + } + return spm_ret; + } else { + entry->data.unknown_item.contents = xmalloc(length); + const ShaDaReadResult fl_ret = fread_len( + sd_reader, entry->data.unknown_item.contents, length); + if (fl_ret != kSDReadStatusSuccess) { + shada_free_shada_entry(entry); + entry->type = kSDItemMissing; + } + return fl_ret; + } + } + + msgpack_unpacked unpacked; + char *buf = NULL; + + const ShaDaReadResult spm_ret = shada_parse_msgpack(sd_reader, length, + &unpacked, &buf); + if (spm_ret != kSDReadStatusSuccess) { + ret = spm_ret; + goto shada_read_next_item_error; + } + ret = kSDReadStatusMalformed; + entry->data = sd_default_values[type_u64].data; + switch ((ShadaEntryType) type_u64) { + case kSDItemHeader: { + if (!msgpack_rpc_to_dictionary(&(unpacked.data), &(entry->data.header))) { + emsgu(_(READERR("header", "is not a dictionary")), initial_fpos); + goto shada_read_next_item_error; + } + break; + } + case kSDItemSearchPattern: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgu(_(READERR("search pattern", "is not a dictionary")), + initial_fpos); + goto shada_read_next_item_error; + } + 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", SEARCH_KEY_MAGIC, + entry->data.search_pattern.magic) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_SMARTCASE, + entry->data.search_pattern.smartcase) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_HAS_LINE_OFFSET, + entry->data.search_pattern.has_line_offset) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_PLACE_CURSOR_AT_END, + entry->data.search_pattern.place_cursor_at_end) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_IS_LAST_USED, + entry->data.search_pattern.is_last_used) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_IS_SUBSTITUTE_PATTERN, + entry->data.search_pattern.is_substitute_pattern) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_HIGHLIGHTED, + entry->data.search_pattern.highlighted) + else + INTEGER_KEY("search pattern", SEARCH_KEY_OFFSET, + entry->data.search_pattern.offset) + else + CONVERTED_STRING_KEY("search pattern", SEARCH_KEY_PAT, + entry->data.search_pattern.pat) + else + ADDITIONAL_KEY + } + if (entry->data.search_pattern.pat == NULL) { + emsgu(_(READERR("search pattern", "has no pattern")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + SET_ADDITIONAL_DATA(entry->data.search_pattern.additional_data, + "search pattern"); + break; + } + case kSDItemChange: + case kSDItemJump: + case kSDItemGlobalMark: + case kSDItemLocalMark: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgu(_(READERR("mark", "is not a dictionary")), initial_fpos); + goto shada_read_next_item_error; + } + 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"); + if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, KEY_NAME_CHAR)) { + if (type_u64 == kSDItemJump || type_u64 == kSDItemChange) { + emsgu(_(READERR("mark", "has n key which is only valid for " + "local and global mark entries")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + CHECKED_ENTRY( + (unpacked.data.via.map.ptr[i].val.type + == MSGPACK_OBJECT_POSITIVE_INTEGER), + "has n key value which is not an unsigned integer", + "mark", unpacked.data.via.map.ptr[i].val, + entry->data.filemark.name, u64, TOCHAR); + } else { + LONG_KEY("mark", KEY_LNUM, entry->data.filemark.mark.lnum) + else + INTEGER_KEY("mark", KEY_COL, entry->data.filemark.mark.col) + else + STRING_KEY("mark", KEY_FILE, entry->data.filemark.fname) + else + ADDITIONAL_KEY + } + } + if (entry->data.filemark.fname == NULL) { + emsgu(_(READERR("mark", "is missing file name")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (entry->data.filemark.mark.lnum <= 0) { + emsgu(_(READERR("mark", "has invalid line number")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (entry->data.filemark.mark.col < 0) { + emsgu(_(READERR("mark", "has invalid column number")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + SET_ADDITIONAL_DATA(entry->data.filemark.additional_data, "mark"); + break; + } + case kSDItemRegister: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgu(_(READERR("register", "is not a dictionary")), initial_fpos); + goto shada_read_next_item_error; + } + 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", REG_KEY_TYPE, "an unsigned integer", + entry->data.reg.type, POSITIVE_INTEGER, u64, TOU8) + else + TYPED_KEY("register", KEY_NAME_CHAR, "an unsigned integer", + entry->data.reg.name, POSITIVE_INTEGER, u64, TOCHAR) + else + TYPED_KEY("register", REG_KEY_WIDTH, "an unsigned integer", + entry->data.reg.width, POSITIVE_INTEGER, u64, TOSIZE) + else + if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, + REG_KEY_CONTENTS)) { + if (unpacked.data.via.map.ptr[i].val.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR( + "register", + "has " REG_KEY_CONTENTS " key with non-array value")), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (unpacked.data.via.map.ptr[i].val.via.array.size == 0) { + emsgu(_(READERR("register", + "has " REG_KEY_CONTENTS " key with empty array")), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + 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) { + emsgu(_(READERR("register", "has " REG_KEY_CONTENTS " array " + "with non-binary value")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + } + 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] = BIN_CONVERTED(arr.ptr[i].via.bin); + } + } else { + ADDITIONAL_KEY + } + } + if (entry->data.reg.contents == NULL) { + emsgu(_(READERR("register", "has missing " REG_KEY_CONTENTS " array")), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + SET_ADDITIONAL_DATA(entry->data.reg.additional_data, "register"); + break; + } + case kSDItemHistoryEntry: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR("history", "is not an array")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size < 2) { + emsgu(_(READERR("history", "does not have enough elements")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type + != MSGPACK_OBJECT_POSITIVE_INTEGER) { + emsgu(_(READERR("history", "has wrong history type type")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[1].type + != MSGPACK_OBJECT_BIN) { + emsgu(_(READERR("history", "has wrong history string type")), + initial_fpos); + goto shada_read_next_item_error; + } + if (memchr(unpacked.data.via.array.ptr[1].via.bin.ptr, 0, + unpacked.data.via.array.ptr[1].via.bin.size) != NULL) { + emsgu(_(READERR("history", "contains string with zero byte inside")), + initial_fpos); + goto shada_read_next_item_error; + } + entry->data.history_item.histtype = + (uint8_t) unpacked.data.via.array.ptr[0].via.u64; + const bool is_hist_search = + entry->data.history_item.histtype == HIST_SEARCH; + if (is_hist_search) { + if (unpacked.data.via.array.size < 3) { + emsgu(_(READERR("search history", + "does not have separator character")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[2].type + != MSGPACK_OBJECT_POSITIVE_INTEGER) { + emsgu(_(READERR("search history", + "has wrong history separator type")), initial_fpos); + goto shada_read_next_item_error; + } + entry->data.history_item.sep = + (char) unpacked.data.via.array.ptr[2].via.u64; + } + size_t strsize; + if (sd_reader->sd_conv.vc_type == CONV_NONE + || !has_non_ascii_len(unpacked.data.via.array.ptr[1].via.bin.ptr, + unpacked.data.via.array.ptr[1].via.bin.size)) { +shada_read_next_item_hist_no_conv: + strsize = ( + unpacked.data.via.array.ptr[1].via.bin.size + + 1 // Zero byte + + 1); // Separator character + entry->data.history_item.string = xmalloc(strsize); + memcpy(entry->data.history_item.string, + unpacked.data.via.array.ptr[1].via.bin.ptr, + unpacked.data.via.array.ptr[1].via.bin.size); + } else { + size_t len = unpacked.data.via.array.ptr[1].via.bin.size; + char *const converted = string_convert( + &sd_reader->sd_conv, unpacked.data.via.array.ptr[1].via.bin.ptr, + &len); + if (converted != NULL) { + strsize = len + 2; + entry->data.history_item.string = xrealloc(converted, strsize); + } else { + goto shada_read_next_item_hist_no_conv; + } + } + entry->data.history_item.string[strsize - 2] = 0; + entry->data.history_item.string[strsize - 1] = + entry->data.history_item.sep; + SET_ADDITIONAL_ELEMENTS(unpacked.data.via.array, (2 + is_hist_search), + entry->data.history_item.additional_elements, + "history"); + break; + } + case kSDItemVariable: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR("variable", "is not an array")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size < 2) { + emsgu(_(READERR("variable", "does not have enough elements")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type != MSGPACK_OBJECT_BIN) { + emsgu(_(READERR("variable", "has wrong variable name type")), + initial_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) { + emsgu(_(READERR("variable", "has wrong variable value type")), + initial_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_to_vim(unpacked.data.via.array.ptr[1], + &(entry->data.global_var.value)) == FAIL) { + emsgu(_(READERR("variable", "has value that cannot " + "be converted to the VimL value")), initial_fpos); + goto shada_read_next_item_error; + } + if (sd_reader->sd_conv.vc_type != CONV_NONE) { + typval_T tgttv; + var_item_copy(&sd_reader->sd_conv, + &entry->data.global_var.value, + &tgttv, + true, + 0); + clear_tv(&entry->data.global_var.value); + entry->data.global_var.value = tgttv; + } + SET_ADDITIONAL_ELEMENTS(unpacked.data.via.array, 2, + entry->data.global_var.additional_elements, + "variable"); + break; + } + case kSDItemSubString: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR("sub string", "is not an array")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size < 1) { + emsgu(_(READERR("sub string", "does not have enough elements")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type != MSGPACK_OBJECT_BIN) { + emsgu(_(READERR("sub string", "has wrong sub string type")), + initial_fpos); + goto shada_read_next_item_error; + } + entry->data.sub_string.sub = + BIN_CONVERTED(unpacked.data.via.array.ptr[0].via.bin); + SET_ADDITIONAL_ELEMENTS(unpacked.data.via.array, 1, + entry->data.sub_string.additional_elements, + "sub string"); + break; + } + case kSDItemBufferList: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR("buffer list", "is not an array")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size == 0) { + break; + } + entry->data.buffer_list.buffers = + xcalloc(unpacked.data.via.array.size, + sizeof(*entry->data.buffer_list.buffers)); + for (size_t i = 0; i < unpacked.data.via.array.size; i++) { + entry->data.buffer_list.size++; + msgpack_unpacked unpacked_2 = (msgpack_unpacked) { + .data = unpacked.data.via.array.ptr[i], + }; + { + msgpack_unpacked unpacked = unpacked_2; + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgu(_(RERR "Error while reading ShaDa file: " + "buffer list at position %" PRIu64 " " + "contains entry that is not a dictionary"), + initial_fpos); + goto shada_read_next_item_error; + } + entry->data.buffer_list.buffers[i].pos = default_pos; + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + { + const size_t j = i; + { + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("buffer list entry"); + LONG_KEY("buffer list entry", KEY_LNUM, + entry->data.buffer_list.buffers[j].pos.lnum) + else + INTEGER_KEY("buffer list entry", KEY_COL, + entry->data.buffer_list.buffers[j].pos.col) + else + STRING_KEY("buffer list entry", KEY_FILE, + entry->data.buffer_list.buffers[j].fname) + else + ADDITIONAL_KEY + } + } + } + if (entry->data.buffer_list.buffers[i].pos.lnum <= 0) { + emsgu(_(RERR "Error while reading ShaDa file: " + "buffer list at position %" PRIu64 " " + "contains entry with invalid line number"), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (entry->data.buffer_list.buffers[i].pos.col < 0) { + emsgu(_(RERR "Error while reading ShaDa file: " + "buffer list at position %" PRIu64 " " + "contains entry with invalid column number"), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (entry->data.buffer_list.buffers[i].fname == NULL) { + emsgu(_(RERR "Error while reading ShaDa file: " + "buffer list at position %" PRIu64 " " + "contains entry that does not have a file name"), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + SET_ADDITIONAL_DATA( + entry->data.buffer_list.buffers[i].additional_data, + "buffer list entry"); + } + } + break; + } + case kSDItemMissing: + case kSDItemUnknown: { + assert(false); + } + } + entry->type = (ShadaEntryType) type_u64; + ret = kSDReadStatusSuccess; +shada_read_next_item_end: + if (buf != NULL) { + msgpack_unpacked_destroy(&unpacked); + xfree(buf); + } + return ret; +shada_read_next_item_error: + entry->type = (ShadaEntryType) type_u64; + shada_free_shada_entry(entry); + entry->type = kSDItemMissing; + goto shada_read_next_item_end; +} +#undef BIN_CONVERTED +#undef CONVERTED +#undef CHECK_KEY +#undef BOOLEAN_KEY +#undef CONVERTED_STRING_KEY +#undef STRING_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 +#undef SET_ADDITIONAL_DATA +#undef SET_ADDITIONAL_ELEMENTS +#undef CLEAR_GA_AND_ERROR_OUT + +/// Check whether "name" is on removable media (according to 'shada') +/// +/// @param[in] name Checked name. +/// +/// @return True if it is, false otherwise. +static bool shada_removable(const char *name) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + char *p; + char part[MAXPATHL + 1]; + bool retval = false; + + char *new_name = home_replace_save(NULL, name); + for (p = (char *) p_shada; *p; ) { + (void) copy_option_part(&p, part, ARRAY_SIZE(part), ", "); + if (part[0] == 'r') { + home_replace(NULL, part + 1, NameBuff, MAXPATHL, true); + size_t n = STRLEN(NameBuff); + if (mb_strnicmp(NameBuff, new_name, n) == 0) { + retval = true; + break; + } + } + } + xfree(new_name); + return retval; +} diff --git a/src/nvim/shada.h b/src/nvim/shada.h new file mode 100644 index 0000000000..49986ac1c1 --- /dev/null +++ b/src/nvim/shada.h @@ -0,0 +1,7 @@ +#ifndef NVIM_SHADA_H +#define NVIM_SHADA_H + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "shada.h.generated.h" +#endif +#endif // NVIM_SHADA_H diff --git a/src/nvim/strings.c b/src/nvim/strings.c index b876753d57..9ffa5c6a76 100644 --- a/src/nvim/strings.c +++ b/src/nvim/strings.c @@ -483,6 +483,21 @@ bool has_non_ascii(const char_u *s) return false; } +/// Return true if string "s" contains a non-ASCII character (128 or higher). +/// When "s" is NULL false is returned. +bool has_non_ascii_len(const char *const s, const size_t len) + FUNC_ATTR_PURE +{ + if (s != NULL) { + for (size_t i = 0; i < len; i++) { + if ((uint8_t) s[i] >= 128) { + return true; + } + } + } + return false; +} + /* * Concatenate two strings and return the result in allocated memory. */ 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/testdir/test8.in b/src/nvim/testdir/test8.in index 0f27c813ec..41e6262e92 100644 --- a/src/nvim/testdir/test8.in +++ b/src/nvim/testdir/test8.in @@ -32,7 +32,7 @@ endfunc $put ='VimLeave done' write endfunc -:set viminfo='100 +:set shada='100 :au BufUnload * call CloseAll() :au VimLeave * call WriteToOut() :e small.vim diff --git a/src/nvim/undo.c b/src/nvim/undo.c index 063c13084d..2b0ffefa7e 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -325,6 +325,14 @@ 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++) { + dict_unref(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 +475,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 +795,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]; @@ -831,8 +841,11 @@ static u_header_T *unserialize_uhp(bufinfo_T *bi, char_u *file_name) unserialize_pos(bi, &uhp->uh_cursor); uhp->uh_cursor_vcol = undo_read_4c(bi); uhp->uh_flags = undo_read_2c(bi); + const Timestamp cur_timestamp = os_time(); 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 = cur_timestamp; + uhp->uh_namedm[i].fnum = 0; } unserialize_visualinfo(bi, &uhp->uh_visual); uhp->uh_time = undo_read_time(bi); @@ -2009,7 +2022,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 +2042,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 +2172,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); |