From 7b9c063d116064bb0765c76277fe210c7ac01d0c Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 27 Sep 2025 09:09:03 +0800 Subject: [PATCH 1/4] vim-patch:9.1.1797: completion: autocompletion can be improved Problem: completion: autocompletion can be improved Solution: Add support for "longest" and "preinsert" in 'autocomplete'; add preinserted() (Girish Palya) * Add support for "longest" in 'completeopt' when 'autocomplete' is enabled. (Note: the cursor position does not change automatically when 'autocomplete' is enabled.) * Add support for "preinsert" when 'autocomplete' is enabled. Ensure "preinsert" works the same with and without 'autocomplete' * introduce the preinserted() Vim script function, useful for defining custom key mappings. fixes: vim/vim#18314 closes: vim/vim#18387 https://github.com/vim/vim/commit/c05335082adb21d99d96374779856444a3a0688f Co-authored-by: Girish Palya --- runtime/doc/options.txt | 42 +++-- runtime/doc/usr_41.txt | 1 + runtime/doc/vimfn.txt | 9 + runtime/lua/vim/_meta/options.lua | 42 +++-- runtime/lua/vim/_meta/vimfn.lua | 8 + src/nvim/edit.c | 8 +- src/nvim/eval.lua | 12 ++ src/nvim/insexpand.c | 140 ++++++++------- src/nvim/options.lua | 42 +++-- test/functional/editor/completion_spec.lua | 8 +- test/old/testdir/test_ins_complete.vim | 199 ++++++++++++++++++++- 11 files changed, 381 insertions(+), 130 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index d82ebb71b3..2a6751b250 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1638,11 +1638,22 @@ A jump table for the options with a short description can be found at |Q_op|. to gather more alternatives for your candidate list, see 'completefuzzycollect'. - longest Only insert the longest common text of the matches. If - the menu is displayed you can use CTRL-L to add more - characters. Whether case is ignored depends on the kind - of completion. For buffer text the 'ignorecase' option is - used. + longest + When 'autocomplete' is not active, only the longest + common prefix of the matches is inserted. If the popup + menu is displayed, you can use CTRL-L to add more + characters. Whether case is ignored depends on the type + of completion. For buffer text the 'ignorecase' option + applies. + + When 'autocomplete' is active and no completion item is + selected, the longest common prefix of the matches is + inserted after the cursor. The prefix is taken either + from all displayed items or only from items in the current + buffer. The inserted text is highlighted with + |hl-PreInsert|, and the cursor position does not change + (similar to `"preinsert"`). Press CTRL-Y to accept. + See also |preinserted()|. menu Use a popup menu to show the possible completions. The menu is only shown when there is more than one match and @@ -1675,22 +1686,21 @@ A jump table for the options with a short description can be found at |Q_op|. with "menu" or "menuone". Overrides "preview". preinsert - When 'autocomplete' is not active, inserts the part of the - first candidate word beyond the current completion leader, - highlighted with |hl-PreInsert|. The cursor doesn't move. - Requires "fuzzy" unset and "menuone" in 'completeopt'. - - When 'autocomplete' is active, inserts the longest common - prefix of matches (from all shown items or from the - current buffer items). This occurs only when no menu item - is selected. Press CTRL-Y to accept. + Inserts the text of the first completion candidate + beyond the current leader, highlighted with |hl-PreInsert|. + The cursor does not move. + Requires "fuzzy" to be unset, and either "menuone" in + 'completeopt' or 'autocomplete' enabled. When + 'autocomplete' is enabled, this does not work if + 'ignorecase' is set without 'infercase'. + See also |preinserted()|. preview Show extra information about the currently selected completion in the preview window. Only works in combination with "menu" or "menuone". - Only "fuzzy", "popup", "preinsert" and "preview" have an effect when - 'autocomplete' is enabled. + Only "fuzzy", "longest", "popup", "preinsert" and "preview" have an + effect when 'autocomplete' is enabled. This option does not apply to |cmdline-completion|. See 'wildoptions' for that. diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index fc2512d5d8..629885ebbf 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -940,6 +940,7 @@ Insert mode completion: *completion-functions* complete_info() get current completion information complete_match() get insert completion start match col and trigger text + preinserted() check if text is inserted after cursor pumvisible() check if the popup menu is displayed pum_getpos() position and size of popup menu if visible diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index 60962561e7..873c871168 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -7232,6 +7232,15 @@ pow({x}, {y}) *pow()* Return: ~ (`number`) +preinserted() *preinserted()* + Returns non-zero if text has been inserted after the cursor + because "preinsert" is present in 'completeopt', or if + "longest" is present in 'completeopt' while 'autocomplete' + is enabled. Otherwise returns zero. + + Return: ~ + (`number`) + prevnonblank({lnum}) *prevnonblank()* Return the line number of the first line at or above {lnum} that is not blank. Example: >vim diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 6323d11d4c..4adad760e9 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -1183,11 +1183,22 @@ vim.go.cia = vim.go.completeitemalign --- to gather more alternatives for your candidate list, --- see 'completefuzzycollect'. --- ---- longest Only insert the longest common text of the matches. If ---- the menu is displayed you can use CTRL-L to add more ---- characters. Whether case is ignored depends on the kind ---- of completion. For buffer text the 'ignorecase' option is ---- used. +--- longest +--- When 'autocomplete' is not active, only the longest +--- common prefix of the matches is inserted. If the popup +--- menu is displayed, you can use CTRL-L to add more +--- characters. Whether case is ignored depends on the type +--- of completion. For buffer text the 'ignorecase' option +--- applies. +--- +--- When 'autocomplete' is active and no completion item is +--- selected, the longest common prefix of the matches is +--- inserted after the cursor. The prefix is taken either +--- from all displayed items or only from items in the current +--- buffer. The inserted text is highlighted with +--- `hl-PreInsert`, and the cursor position does not change +--- (similar to `"preinsert"`). Press CTRL-Y to accept. +--- See also `preinserted()`. --- --- menu Use a popup menu to show the possible completions. The --- menu is only shown when there is more than one match and @@ -1220,22 +1231,21 @@ vim.go.cia = vim.go.completeitemalign --- with "menu" or "menuone". Overrides "preview". --- --- preinsert ---- When 'autocomplete' is not active, inserts the part of the ---- first candidate word beyond the current completion leader, ---- highlighted with `hl-PreInsert`. The cursor doesn't move. ---- Requires "fuzzy" unset and "menuone" in 'completeopt'. ---- ---- When 'autocomplete' is active, inserts the longest common ---- prefix of matches (from all shown items or from the ---- current buffer items). This occurs only when no menu item ---- is selected. Press CTRL-Y to accept. +--- Inserts the text of the first completion candidate +--- beyond the current leader, highlighted with `hl-PreInsert`. +--- The cursor does not move. +--- Requires "fuzzy" to be unset, and either "menuone" in +--- 'completeopt' or 'autocomplete' enabled. When +--- 'autocomplete' is enabled, this does not work if +--- 'ignorecase' is set without 'infercase'. +--- See also `preinserted()`. --- --- preview Show extra information about the currently selected --- completion in the preview window. Only works in --- combination with "menu" or "menuone". --- ---- Only "fuzzy", "popup", "preinsert" and "preview" have an effect when ---- 'autocomplete' is enabled. +--- Only "fuzzy", "longest", "popup", "preinsert" and "preview" have an +--- effect when 'autocomplete' is enabled. --- --- This option does not apply to `cmdline-completion`. See 'wildoptions' --- for that. diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 4302c3d689..4b50b90f65 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -6554,6 +6554,14 @@ function vim.fn.perleval(expr) end --- @return number function vim.fn.pow(x, y) end +--- Returns non-zero if text has been inserted after the cursor +--- because "preinsert" is present in 'completeopt', or if +--- "longest" is present in 'completeopt' while 'autocomplete' +--- is enabled. Otherwise returns zero. +--- +--- @return number +function vim.fn.preinserted() end + --- Return the line number of the first line at or above {lnum} --- that is not blank. Example: >vim --- let ind = indent(prevnonblank(v:lnum - 1)) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 1375c5c208..4f2ff39c6f 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -606,10 +606,10 @@ static int insert_execute(VimState *state, int key) && (s->c == CAR || s->c == K_KENTER || s->c == NL))) && stop_arrow() == OK) { ins_compl_delete(false); - if (ins_compl_has_preinsert() && ins_compl_autocomplete_enabled()) { - (void)ins_compl_insert(false, true); - } else { - (void)ins_compl_insert(false, false); + ins_compl_insert(false, !ins_compl_has_preinsert()); + if (ins_compl_preinsert_longest()) { + ins_compl_init_get_longest(); + return 1; } } else if (ascii_iswhite_nl_or_nul(s->c) && ins_compl_preinsert_effect()) { // Delete preinserted text when typing special chars diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 4e7c4b3da4..eccca651a8 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -8024,6 +8024,18 @@ M.funcs = { returns = 'number', signature = 'pow({x}, {y})', }, + preinserted = { + desc = [=[ + Returns non-zero if text has been inserted after the cursor + because "preinsert" is present in 'completeopt', or if + "longest" is present in 'completeopt' while 'autocomplete' + is enabled. Otherwise returns zero. + ]=], + name = 'preinserted', + params = {}, + returns = 'number', + signature = 'preinserted()', + }, prevnonblank = { args = 1, base = 1, diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 9b87112841..9a4d978079 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -296,7 +296,7 @@ static bool compl_autocomplete = false; ///< whether autocompletion is ac static uint64_t compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS; static bool compl_time_slice_expired = false; ///< time budget exceeded for current source static bool compl_from_nonkeyword = false; ///< completion started from non-keyword -static bool compl_autocomplete_preinsert = false; ///< apply preinsert highlight +static bool compl_hi_on_autocompl_longest = false; ///< apply "PreInsert" highlight // Halve the current completion timeout, simulating exponential decay. #define COMPL_MIN_TIMEOUT_MS 5 @@ -889,6 +889,15 @@ static bool is_nearest_active(void) && !(flags & kOptCotFlagFuzzy); } +/// Returns true if autocomplete is active and the pre-insert effect targets the +/// longest prefix. +bool ins_compl_preinsert_longest(void) +{ + return compl_autocomplete + && (get_cot_flags() & (kOptCotFlagLongest | kOptCotFlagPreinsert | kOptCotFlagFuzzy)) + == kOptCotFlagLongest; +} + /// Add a match to the list of matches /// /// @param[in] str text of the match to add @@ -1053,7 +1062,8 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons compl_curr_match = match; // Find the longest common string if still doing that. - if (compl_get_longest && (flags & CP_ORIGINAL_TEXT) == 0 && !cfc_has_mode()) { + if (compl_get_longest && (flags & CP_ORIGINAL_TEXT) == 0 && !cfc_has_mode() + && !ins_compl_preinsert_longest()) { ins_compl_longest_match(match); } @@ -1107,17 +1117,12 @@ static size_t ins_compl_leader_len(void) /// -1 means normal item. int ins_compl_col_range_attr(linenr_T lnum, int col) { - const bool has_preinsert = ins_compl_has_preinsert(); + const bool has_preinsert = ins_compl_has_preinsert() || ins_compl_preinsert_longest(); int attr; if ((get_cot_flags() & kOptCotFlagFuzzy) - || (!has_preinsert - && (attr = syn_name2attr("ComplMatchIns")) == 0) - || (!compl_autocomplete && has_preinsert - && (attr = syn_name2attr("PreInsert")) == 0) - || (compl_autocomplete - && (!compl_autocomplete_preinsert - || (attr = syn_name2attr("PreInsert")) == 0))) { + || (!compl_hi_on_autocompl_longest && ins_compl_preinsert_longest()) + || (attr = syn_name2attr(has_preinsert ? "PreInsert" : "ComplMatchIns")) == 0) { return -1; } @@ -1511,7 +1516,8 @@ static int ins_compl_build_pum(void) } unsigned cur_cot_flags = get_cot_flags(); - bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0 || compl_autocomplete; + bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0 + || (compl_autocomplete && !ins_compl_has_preinsert()); bool fuzzy_filter = (cur_cot_flags & kOptCotFlagFuzzy) != 0; compl_T *match_head = NULL, *match_tail = NULL; @@ -2132,6 +2138,9 @@ int ins_compl_len(void) bool ins_compl_has_preinsert(void) { unsigned cur_cot_flags = get_cot_flags(); + if (compl_autocomplete && p_ic && !p_inf) { + return false; + } return (!compl_autocomplete ? (cur_cot_flags & (kOptCotFlagPreinsert|kOptCotFlagFuzzy|kOptCotFlagMenuone)) == (kOptCotFlagPreinsert|kOptCotFlagMenuone) @@ -2143,19 +2152,13 @@ bool ins_compl_has_preinsert(void) /// the `compl_ins_end_col` range. bool ins_compl_preinsert_effect(void) { - if (!ins_compl_has_preinsert()) { + if (!ins_compl_has_preinsert() && !ins_compl_preinsert_longest()) { return false; } return curwin->w_cursor.col < compl_ins_end_col; } -/// Returns true if autocompletion is active. -bool ins_compl_autocomplete_enabled(void) -{ - return compl_autocomplete; -} - /// Delete one character before the cursor and show the subset of the matches /// that match the word that is now before the cursor. /// Returns the character to be used, NUL if the work is done and another char @@ -2198,7 +2201,7 @@ int ins_compl_bs(void) (size_t)(p_off - (ptrdiff_t)compl_col)); // Clear selection if a menu item is currently selected in autocompletion - if (compl_autocomplete && compl_first_match) { + if (compl_autocomplete && compl_first_match && !ins_compl_has_preinsert()) { compl_shown_match = compl_first_match; } @@ -2291,18 +2294,14 @@ static void ins_compl_new_leader(void) // Show the popup menu with a different set of matches. ins_compl_show_pum(); - compl_autocomplete_preinsert = false; // Don't let Enter select the original text when there is no popup menu. if (compl_match_array == NULL) { compl_enter_selects = false; } else if (ins_compl_has_preinsert() && compl_leader.size > 0) { - if (compl_started && compl_autocomplete && !ins_compl_preinsert_effect()) { - if (ins_compl_insert(true, true) == OK) { - compl_autocomplete_preinsert = true; - } - } else { - (void)ins_compl_insert(true, false); - } + ins_compl_insert(true, false); + } else if (compl_started && ins_compl_preinsert_longest() + && compl_leader.size > 0 && !ins_compl_preinsert_effect()) { + ins_compl_insert(true, true); } // Don't let Enter select when use user function and refresh_always is set if (ins_compl_refresh_always()) { @@ -4388,7 +4387,7 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ ptr = ins_compl_get_next_word_or_line(st->ins_buf, st->cur_match_pos, &len, &cont_s_ipos); } - if (ptr == NULL || (!compl_autocomplete && ins_compl_has_preinsert() + if (ptr == NULL || (ins_compl_has_preinsert() && strcmp(ptr, compl_pattern.data) == 0)) { continue; } @@ -4895,7 +4894,7 @@ static int ins_compl_get_exp(pos_T *ini) } may_trigger_modechanged(); - if (is_nearest_active()) { + if (is_nearest_active() && !ins_compl_has_preinsert()) { sort_compl_match_list(cp_compare_nearest); } @@ -5108,6 +5107,21 @@ static char *find_common_prefix(size_t *prefix_len, bool curbuf_only) xfree(match_count); if (len > (int)ins_compl_leader_len()) { + assert(first != NULL); + // Avoid inserting text that duplicates the text already present + // after the cursor. + if (len == (int)strlen(first)) { + char *line = get_cursor_line_ptr(); + char *p = line + curwin->w_cursor.col; + if (p && !ascii_iswhite_or_nul(*p)) { + char *end = find_word_end(p); + int text_len = (int)(end - p); + if (text_len > 0 && text_len < (len - (int)ins_compl_leader_len()) + && strncmp(first + len - text_len, p, (size_t)text_len) == 0) { + len -= text_len; + } + } + } *prefix_len = (size_t)len; return first; } @@ -5117,9 +5131,9 @@ static char *find_common_prefix(size_t *prefix_len, bool curbuf_only) /// Insert the new text being completed. /// "move_cursor" is used when 'completeopt' includes "preinsert" and when true /// cursor needs to move back from the inserted text to the compl_leader. -/// When "preinsert_prefix" is true the longest common prefix is inserted -/// instead of shown match. -int ins_compl_insert(bool move_cursor, bool preinsert_prefix) +/// When "insert_prefix" is true the longest common prefix is inserted instead +/// of shown match. +void ins_compl_insert(bool move_cursor, bool insert_prefix) { int compl_len = get_compl_len(); bool preinsert = ins_compl_has_preinsert(); @@ -5128,12 +5142,13 @@ int ins_compl_insert(bool move_cursor, bool preinsert_prefix) size_t leader_len = ins_compl_leader_len(); char *has_multiple = strchr(cp_str, '\n'); - if (preinsert_prefix) { + if (insert_prefix) { cp_str = find_common_prefix(&cp_str_len, false); if (cp_str == NULL) { cp_str = find_common_prefix(&cp_str_len, true); if (cp_str == NULL) { - return FAIL; + cp_str = compl_shown_match->cp_str.data; + cp_str_len = compl_shown_match->cp_str.size; } } } else if (cpt_sources_array != NULL) { @@ -5160,18 +5175,18 @@ int ins_compl_insert(bool move_cursor, bool preinsert_prefix) ins_compl_expand_multiple(cp_str + compl_len); } else { ins_compl_insert_bytes(cp_str + compl_len, - preinsert_prefix ? (int)cp_str_len - compl_len : -1); - if (preinsert && move_cursor) { + insert_prefix ? (int)cp_str_len - compl_len : -1); + if ((preinsert || insert_prefix) && move_cursor) { curwin->w_cursor.col -= (colnr_T)(cp_str_len - leader_len); } } } compl_used_match = !(match_at_original_text(compl_shown_match) - || (preinsert && !compl_autocomplete)); + || (preinsert && !insert_prefix)); dict_T *dict = ins_compl_dict_alloc(compl_shown_match); set_vim_var_dict(VV_COMPLETED_ITEM, dict); - return OK; + compl_hi_on_autocompl_longest = insert_prefix && move_cursor; } /// show the file name for the completion match (if any). Truncate the file @@ -5240,7 +5255,8 @@ static int find_next_completion_match(bool allow_get_expansion, int todo, bool a bool found_end = false; compl_T *found_compl = NULL; unsigned cur_cot_flags = get_cot_flags(); - bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0 || compl_autocomplete; + bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0 + || (compl_autocomplete && !ins_compl_has_preinsert()); bool compl_fuzzy_match = (cur_cot_flags & kOptCotFlagFuzzy) != 0; while (--todo >= 0) { @@ -5351,7 +5367,8 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match const bool started = compl_started; buf_T *const orig_curbuf = curbuf; unsigned cur_cot_flags = get_cot_flags(); - bool compl_no_insert = (cur_cot_flags & kOptCotFlagNoinsert) != 0 || compl_autocomplete; + bool compl_no_insert = (cur_cot_flags & kOptCotFlagNoinsert) != 0 + || (compl_autocomplete && !ins_compl_has_preinsert()); bool compl_fuzzy_match = (cur_cot_flags & kOptCotFlagFuzzy) != 0; bool compl_preinsert = ins_compl_has_preinsert(); @@ -5397,35 +5414,18 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match return -1; } - compl_autocomplete_preinsert = false; - // Insert the text of the new completion, or the compl_leader. - if (compl_no_insert && !started) { - bool insert_orig = !compl_preinsert; - if (compl_preinsert && compl_autocomplete) { - if (ins_compl_insert(true, true) == OK) { - compl_autocomplete_preinsert = true; - } else { - insert_orig = true; - } - } - if (insert_orig) { - ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1); - } + if (!started && ins_compl_preinsert_longest()) { + ins_compl_insert(true, true); + } else if (compl_no_insert && !started && !compl_preinsert) { + ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1); compl_used_match = false; restore_orig_extmarks(); } else if (insert_match) { if (!compl_get_longest || compl_used_match) { - bool none_selected = match_at_original_text(compl_shown_match); - if (compl_preinsert && compl_autocomplete && none_selected) { - if (ins_compl_insert(none_selected, true) == OK) { - compl_autocomplete_preinsert = none_selected; - } else { - (void)ins_compl_insert(false, false); - } - } else { - (void)ins_compl_insert(!compl_autocomplete, false); - } + bool preinsert_longest = ins_compl_preinsert_longest() + && match_at_original_text(compl_shown_match); // none selected + ins_compl_insert(compl_preinsert || preinsert_longest, preinsert_longest); } else { assert(compl_leader.data != NULL); ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1); @@ -5534,8 +5534,8 @@ void ins_compl_check_keys(int frequency, bool in_compl_func) } } - if (compl_pending != 0 && !got_int && !(cot_flags & kOptCotFlagNoinsert) - && !compl_autocomplete) { + if (compl_pending && !got_int && !(cot_flags & kOptCotFlagNoinsert) + && (!compl_autocomplete || ins_compl_has_preinsert())) { // Insert the first match immediately and advance compl_shown_match, // before finding other matches. int todo = compl_pending > 0 ? compl_pending : -compl_pending; @@ -6599,3 +6599,11 @@ static void cpt_compl_refresh(void) // Make the list cyclic compl_matches = ins_compl_make_cyclic(); } + +/// "preinserted()" function +void f_preinserted(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + if (ins_compl_preinsert_effect()) { + rettv->vval.v_number = 1; + } +} diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 4f1d03e57e..e4a53cb982 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1663,11 +1663,22 @@ local options = { to gather more alternatives for your candidate list, see 'completefuzzycollect'. - longest Only insert the longest common text of the matches. If - the menu is displayed you can use CTRL-L to add more - characters. Whether case is ignored depends on the kind - of completion. For buffer text the 'ignorecase' option is - used. + longest + When 'autocomplete' is not active, only the longest + common prefix of the matches is inserted. If the popup + menu is displayed, you can use CTRL-L to add more + characters. Whether case is ignored depends on the type + of completion. For buffer text the 'ignorecase' option + applies. + + When 'autocomplete' is active and no completion item is + selected, the longest common prefix of the matches is + inserted after the cursor. The prefix is taken either + from all displayed items or only from items in the current + buffer. The inserted text is highlighted with + |hl-PreInsert|, and the cursor position does not change + (similar to `"preinsert"`). Press CTRL-Y to accept. + See also |preinserted()|. menu Use a popup menu to show the possible completions. The menu is only shown when there is more than one match and @@ -1700,22 +1711,21 @@ local options = { with "menu" or "menuone". Overrides "preview". preinsert - When 'autocomplete' is not active, inserts the part of the - first candidate word beyond the current completion leader, - highlighted with |hl-PreInsert|. The cursor doesn't move. - Requires "fuzzy" unset and "menuone" in 'completeopt'. - - When 'autocomplete' is active, inserts the longest common - prefix of matches (from all shown items or from the - current buffer items). This occurs only when no menu item - is selected. Press CTRL-Y to accept. + Inserts the text of the first completion candidate + beyond the current leader, highlighted with |hl-PreInsert|. + The cursor does not move. + Requires "fuzzy" to be unset, and either "menuone" in + 'completeopt' or 'autocomplete' enabled. When + 'autocomplete' is enabled, this does not work if + 'ignorecase' is set without 'infercase'. + See also |preinserted()|. preview Show extra information about the currently selected completion in the preview window. Only works in combination with "menu" or "menuone". - Only "fuzzy", "popup", "preinsert" and "preview" have an effect when - 'autocomplete' is enabled. + Only "fuzzy", "longest", "popup", "preinsert" and "preview" have an + effect when 'autocomplete' is enabled. This option does not apply to |cmdline-completion|. See 'wildoptions' for that. diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index 6223648f85..80aea84b3e 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -1496,18 +1496,18 @@ describe('completion', function() -- During delay wait, user can open menu using CTRL_N completion feed('') - command('set completeopt=menuone,preinsert') + command('set completeopt=menuone,longest') feed('Sf') screen:expect([[ foo | foobar | foobarbaz | - f{102:^oo} | - {12:foo }{1: }| + foo^ | + {4:foo }{1: }| {4:foobar }{1: }| {4:foobarbaz }{1: }| {1:~ }|*2 - {5:-- Keyword completion (^N^P) }{6:match 1 of 3} | + {5:-- Keyword completion (^N^P) }{19:Back at original} | ]]) -- After the menu is open, ^N/^P and Up/Down should not delay diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index f06f640647..ca365fbe08 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -124,10 +124,9 @@ endfunc func Test_omni_dash() func Omni(findstart, base) if a:findstart - return 5 + return 5 else - echom a:base - return ['-help', '-v'] + return ['-help', '-v'] endif endfunc set omnifunc=Omni @@ -4231,6 +4230,145 @@ func Test_completeopt_preinsert() delfunc Omni_test endfunc +func Test_autocomplete_completeopt_preinsert() + func Omni_test(findstart, base) + if a:findstart + return col(".") - 1 + endif + return [#{word: "fobar"}, #{word: "foobar"}] + endfunc + set omnifunc=Omni_test complete+=o + set completeopt=preinsert autocomplete + " set completeopt=preinsert,menuone autocomplete + func GetLine() + let g:line = getline('.') + let g:col = col('.') + endfunc + + call Ntest_override("char_avail", 1) + new + inoremap =GetLine() + call feedkeys("Sfo\\", 'tx') + call assert_equal("fobar", g:line) + call assert_equal(3, g:col) + + call feedkeys("Sfoo\\", 'tx') + call assert_equal("foobar", g:line) + + call feedkeys("Sfoo\\\", 'tx') + call assert_equal("", getline('.')) + + " delete a character + call feedkeys("Sfoo\b\\", 'tx') + call assert_equal("fobar", g:line) + call assert_equal(4, g:col) + + set complete& + %d + call setline(1, ['fobar', 'foobar']) + + call feedkeys("Gofoo\\\\", 'tx') + call assert_equal("fobar", g:line) + call assert_equal(2, g:col) + + call feedkeys("Shello wo\\\f\\", 'tx') + call assert_equal("hello fobar wo", g:line) + call assert_equal(9, g:col) + + call feedkeys("Shello wo\\\f\\\", 'tx') + call assert_equal("hello wo", g:line) + call assert_equal(8, g:col) + + call feedkeys("Shello wo\\\foo\\", 'tx') + call assert_equal("hello foobar wo", g:line) + call assert_equal(11, g:col) + + call feedkeys("Shello wo\\\foo\b\\", 'tx') + call assert_equal("hello fobar wo", g:line) + call assert_equal(11, g:col) + + " confirm + call feedkeys("Sf\", 'tx') + call assert_equal("fobar", getline('.')) + call assert_equal(5, col('.')) + + " cancel + call feedkeys("Sfo\", 'tx') + call assert_equal("fo", getline('.')) + call assert_equal(2, col('.')) + + " delete preinsert part + call feedkeys("Sfo ", 'tx') + call assert_equal("fo ", getline('.')) + call assert_equal(3, col('.')) + + " can not work with fuzzy + set cot+=fuzzy + call feedkeys("Sf", 'tx') + call assert_equal("f", getline('.')) + set cot-=fuzzy + + " does not work with 'ignorecase' unless 'infercase' is also enabled + %d + call setline(1, ['FIX', 'fobar', 'foobar']) + set ignorecase + call feedkeys("Gof\\", 'tx') + call assert_equal("f", g:line) " should not produce 'FIX' + set infercase + call feedkeys("Gof\\", 'tx') + call assert_equal("fix", g:line) + set ignorecase& infercase& + + %delete _ + let &l:undolevels = &l:undolevels + normal! ifoo + let &l:undolevels = &l:undolevels + normal! obar + let &l:undolevels = &l:undolevels + normal! obaz + let &l:undolevels = &l:undolevels + + func CheckUndo() + let g:errmsg = '' + call assert_equal(['foo', 'bar', 'baz'], getline(1, '$')) + undo + call assert_equal(['foo', 'bar'], getline(1, '$')) + undo + call assert_equal(['foo'], getline(1, '$')) + undo + call assert_equal([''], getline(1, '$')) + later 3 + call assert_equal(['foo', 'bar', 'baz'], getline(1, '$')) + call assert_equal('', v:errmsg) + endfunc + + " Check that switching buffer with "preinsert" doesn't corrupt undo. + new + setlocal bufhidden=wipe + inoremap enew! + call feedkeys("if\\", 'tx') + bwipe! + call CheckUndo() + + " Check that closing window with "preinsert" doesn't corrupt undo. + new + setlocal bufhidden=wipe + inoremap close! + call feedkeys("if\\", 'tx') + call CheckUndo() + + %delete _ + delfunc CheckUndo + + bw! + set cot& + set omnifunc& + set autocomplete& + call Ntest_override("char_avail", 0) + delfunc Omni_test + delfunc GetLine +endfunc + " Check that mark positions are correct after triggering multiline completion. func Test_complete_multiline_marks() func Omni_test(findstart, base) @@ -5696,7 +5834,7 @@ func Test_autocompletedelay() call VerifyScreenDump(buf, 'Test_autocompletedelay_6', {}) " During delay wait, user can open menu using CTRL_N completion - call term_sendkeys(buf, "\:set completeopt=menuone,preinsert\") + call term_sendkeys(buf, "\:set completeopt=menuone,longest\") call term_sendkeys(buf, "Sf\") call VerifyScreenDump(buf, 'Test_autocompletedelay_7', {}) @@ -5720,16 +5858,18 @@ func Test_autocompletedelay() call StopVimInTerminal(buf) endfunc -func Test_autocomplete_completeopt_preinsert() +" Preinsert longest prefix when autocomplete +func Test_autocomplete_longest() func GetLine() let g:line = getline('.') let g:col = col('.') + let g:preinserted = preinserted() endfunc call Ntest_override("char_avail", 1) new inoremap =GetLine() - set completeopt=preinsert autocomplete + set completeopt=longest autocomplete call setline(1, ["foobar", "foozbar"]) call feedkeys("Go\", 'tx') @@ -5761,10 +5901,15 @@ func Test_autocomplete_completeopt_preinsert() call DoTest("f\", 'foobar', 7) call DoTest("fo\\", 'foobar', 7) + " to accept preinserted text + inoremap =pumvisible() call DoTest("zar\0f", 'foozar', 2) call DoTest("zar\0f\", 'foozar', 4) + call DoTest("zar\0f\\", 'foo1zar', 5) call DoTest("zar\0f\\", 'foozar', 3) + call DoTest("zar\0f\\\", 'fo1zar', 4) + " Select items in menu call DoTest("zar\0f\", 'foozbarzar', 8) call DoTest("zar\0f\\", 'foobarzar', 7) call DoTest("zar\0f\\\", 'foozar', 2) @@ -5778,6 +5923,34 @@ func Test_autocomplete_completeopt_preinsert() call DoTest("f", 'f', 2) set cot-=fuzzy + " preinserted() + call DoTest("f", 'foo', 2) + call assert_equal(1, g:preinserted) + call assert_equal(0, preinserted()) + call DoTest("f\", '', 1) + call assert_equal(0, g:preinserted) + call DoTest("f ", 'f ', 3) + call assert_equal(0, g:preinserted) + call DoTest("foob", 'foobar', 5) + call assert_equal(1, g:preinserted) + call DoTest("foob\\x", 'fox', 4) + call assert_equal(0, g:preinserted) + + " Complete non-keyword + func Omni(findstart, base) + if a:findstart + return 5 + else + return ['xyz:'] + endif + endfunc + set omnifunc=Omni + set cpt+=o + call DoTest("xyz:", "xyz:xyz:", 5) + call DoTest("xyz:\\", "xyz:", 3) + set omnifunc& cpt& + delfunc Omni + " leader should match prefix of inserted word %delete set smartcase ignorecase @@ -5788,6 +5961,16 @@ func Test_autocomplete_completeopt_preinsert() call assert_equal('FOO', g:line) set smartcase& ignorecase& + " avoid repeating text that is already present after the cursor + %delete + call setline(1, ["foobarbaz", ""]) + call feedkeys($"G", 'tx') + call DoTest("baz\0f", "foobarbaz", 2) + call feedkeys($"Sxfoozar\\", 'tx') + call DoTest("baz\0f", "foobarbaz", 2) + call feedkeys($"Sfoozar\\", 'tx') + call DoTest("baz\0f", "foobaz", 2) + " Verify that redo (dot) works %delete call setline(1, ["foobar", "foozbar", "foobaz", "changed", "change"]) @@ -5824,7 +6007,7 @@ func Test_autocomplete_completeopt_preinsert() call assert_equal('', v:errmsg) endfunc - " Check that switching buffer with "preinsert" doesn't corrupt undo. + " Check that switching buffer with "longest" doesn't corrupt undo. new setlocal bufhidden=wipe inoremap enew! @@ -5832,7 +6015,7 @@ func Test_autocomplete_completeopt_preinsert() bwipe! call CheckUndo() - " Check that closing window with "preinsert" doesn't corrupt undo. + " Check that closing window with "longest" doesn't corrupt undo. new setlocal bufhidden=wipe inoremap close! From e6b2255ac784913ff7da1ea670d28d3ba850f3a8 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Mon, 29 Sep 2025 06:17:37 +0800 Subject: [PATCH 2/4] vim-patch:4edaf89: runtime(doc): improve preinserted() doc Change the second "if" to "because", otherwise it may be misinterpreted that preinserted() can return non-zero just because these options are set. closes: vim/vim#18409 https://github.com/vim/vim/commit/4edaf8923335504c31810dc4c5213eaba84e7898 --- runtime/doc/vimfn.txt | 4 ++-- runtime/lua/vim/_meta/vimfn.lua | 4 ++-- src/nvim/eval.lua | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index 873c871168..515a60e6b3 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -7234,9 +7234,9 @@ pow({x}, {y}) *pow()* preinserted() *preinserted()* Returns non-zero if text has been inserted after the cursor - because "preinsert" is present in 'completeopt', or if + because "preinsert" is present in 'completeopt', or because "longest" is present in 'completeopt' while 'autocomplete' - is enabled. Otherwise returns zero. + is active. Otherwise returns zero. Return: ~ (`number`) diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 4b50b90f65..830882c9c1 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -6555,9 +6555,9 @@ function vim.fn.perleval(expr) end function vim.fn.pow(x, y) end --- Returns non-zero if text has been inserted after the cursor ---- because "preinsert" is present in 'completeopt', or if +--- because "preinsert" is present in 'completeopt', or because --- "longest" is present in 'completeopt' while 'autocomplete' ---- is enabled. Otherwise returns zero. +--- is active. Otherwise returns zero. --- --- @return number function vim.fn.preinserted() end diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index eccca651a8..a028189910 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -8027,9 +8027,9 @@ M.funcs = { preinserted = { desc = [=[ Returns non-zero if text has been inserted after the cursor - because "preinsert" is present in 'completeopt', or if + because "preinsert" is present in 'completeopt', or because "longest" is present in 'completeopt' while 'autocomplete' - is enabled. Otherwise returns zero. + is active. Otherwise returns zero. ]=], name = 'preinserted', params = {}, From f30a33858f23b6bc3c77fef44167e919c4925fdd Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 30 Sep 2025 09:06:09 +0800 Subject: [PATCH 3/4] vim-patch:9.1.1800: completion: strange behaviour with 'ac' completeopt=longest,preinsert Problem: completion: strange behaviour with 'ac' completeopt=longest,preinsert (zeertzjq) Solution: Let preinsert take precedence (Girish Palya) fixes: vim/vim#18410 closes: vim/vim#18428 https://github.com/vim/vim/commit/d35e5e4237ec578962d3d577bc5a4e4f6bf4abb2 Co-authored-by: Girish Palya --- src/nvim/insexpand.c | 3 ++- test/functional/editor/completion_spec.lua | 6 +++--- test/old/testdir/test_ins_complete.vim | 12 +++++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 9a4d978079..aed25df3b9 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -2736,7 +2736,8 @@ bool ins_compl_prep(int c) // Set "compl_get_longest" when finding the first matches. if (ctrl_x_mode_not_defined_yet() || (ctrl_x_mode_normal() && !compl_started)) { - compl_get_longest = (get_cot_flags() & kOptCotFlagLongest) != 0; + compl_get_longest = (get_cot_flags() & kOptCotFlagLongest) != 0 + && !ins_compl_has_autocomplete(); compl_used_match = true; } diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index 80aea84b3e..7dfdb50be9 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -1496,18 +1496,18 @@ describe('completion', function() -- During delay wait, user can open menu using CTRL_N completion feed('') - command('set completeopt=menuone,longest') + command('set completeopt=menuone') feed('Sf') screen:expect([[ foo | foobar | foobarbaz | foo^ | - {4:foo }{1: }| + {12:foo }{1: }| {4:foobar }{1: }| {4:foobarbaz }{1: }| {1:~ }|*2 - {5:-- Keyword completion (^N^P) }{19:Back at original} | + {5:-- Keyword completion (^N^P) }{6:match 1 of 3} | ]]) -- After the menu is open, ^N/^P and Up/Down should not delay diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index ca365fbe08..d362c6ed83 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -5834,7 +5834,7 @@ func Test_autocompletedelay() call VerifyScreenDump(buf, 'Test_autocompletedelay_6', {}) " During delay wait, user can open menu using CTRL_N completion - call term_sendkeys(buf, "\:set completeopt=menuone,longest\") + call term_sendkeys(buf, "\:set completeopt=menuone\") call term_sendkeys(buf, "Sf\") call VerifyScreenDump(buf, 'Test_autocompletedelay_7', {}) @@ -5984,6 +5984,16 @@ func Test_autocomplete_longest() call feedkeys("Go\", 'tx') call DoTest("f\\\\\\", 'foo', 3) + " Issue #18410: When both 'preinsert' and 'longest' are set, 'preinsert' + " takes precedence + %delete + set autocomplete completeopt+=longest,preinsert + call setline(1, ['foobar', 'foofoo', 'foobaz', '']) + call feedkeys("G", 'tx') + call DoTest("f", 'foobar', 2) + call assert_equal(1, g:preinserted) + + " Undo %delete _ let &l:undolevels = &l:undolevels normal! ifoo From 756e55dc490d98f9ec09885bb6261e8160ebf914 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 30 Sep 2025 09:11:07 +0800 Subject: [PATCH 4/4] vim-patch:9.1.1810: completion: "longest" doesn't work for manual completion with 'ac' Problem: completion: "longest" doesn't work for manual completion when 'autocomplete' is on (after 9.1.1800). Solution: Only reset compl_get_longest when enabling autocompletion (zeertzjq). closes: vim/vim#18430 https://github.com/vim/vim/commit/b3966d6a8e96c9de45a344ec3d42fbb156863835 --- src/nvim/insexpand.c | 10 +++++++--- test/old/testdir/test_ins_complete.vim | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index aed25df3b9..1a94684951 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -2266,7 +2266,11 @@ static void ins_compl_new_leader(void) // Matches were cleared, need to search for them now. // Set "compl_restarting" to avoid that the first match is inserted. compl_restarting = true; - compl_autocomplete = ins_compl_has_autocomplete(); + if (ins_compl_has_autocomplete()) { + ins_compl_enable_autocomplete(); + } else { + compl_autocomplete = false; + } if (ins_complete(Ctrl_N, true) == FAIL) { compl_cont_status = 0; } @@ -2736,8 +2740,7 @@ bool ins_compl_prep(int c) // Set "compl_get_longest" when finding the first matches. if (ctrl_x_mode_not_defined_yet() || (ctrl_x_mode_normal() && !compl_started)) { - compl_get_longest = (get_cot_flags() & kOptCotFlagLongest) != 0 - && !ins_compl_has_autocomplete(); + compl_get_longest = (get_cot_flags() & kOptCotFlagLongest) != 0; compl_used_match = true; } @@ -6293,6 +6296,7 @@ int ins_complete(int c, bool enable_pum) void ins_compl_enable_autocomplete(void) { compl_autocomplete = true; + compl_get_longest = false; } /// Remove (if needed) and show the popup menu diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index d362c6ed83..e34e39bd3e 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -5984,7 +5984,7 @@ func Test_autocomplete_longest() call feedkeys("Go\", 'tx') call DoTest("f\\\\\\", 'foo', 3) - " Issue #18410: When both 'preinsert' and 'longest' are set, 'preinsert' + " Issue #18410: When both "preinsert" and "longest" are set, "preinsert" " takes precedence %delete set autocomplete completeopt+=longest,preinsert @@ -6035,6 +6035,21 @@ func Test_autocomplete_longest() %delete _ delfunc CheckUndo + " Check that behavior of "longest" in manual completion is unchanged. + for ac in [v:false, v:true] + let &ac = ac + set completeopt=menuone,longest + call feedkeys("Ssign u\\", 'tx') + call assert_equal('sign un', getline('.')) + call feedkeys("Ssign u\\\", 'tx') + call assert_equal('sign undefine', getline('.')) + call feedkeys("Ssign u\\\\", 'tx') + call assert_equal('sign unplace', getline('.')) + call feedkeys("Ssign u\\\\\", 'tx') + call assert_equal('sign u', getline('.')) + %delete + endfor + bw! set cot& autocomplete& delfunc GetLine