From 08b8fe5ab36ee3df3ce593942565d4cd318373c4 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 20 Feb 2026 08:57:15 +0800 Subject: [PATCH 1/2] vim-patch:9.2.0030: completion: non-prefix matches shown when leader is NULL Problem: When 'autocomplete' fires before compl_leader is initialized, the prefix filter is bypassed. This allows non-prefix matches (e.g. from fuzzy omnifuncs) to be shown in the popup menu and incorrectly preinserted. Solution: In get_leader_for_startcol(), if compl_leader.string is NULL, fall back to using compl_orig_text as a filter for matches starting at or after the completion column (Hirohito Higashi). When 'autocomplete' first fires, compl_leader is NULL because ins_compl_start() has not set it yet. This caused the prefix filter in ins_compl_build_pum(), find_next_completion_match() and find_common_prefix() to be bypassed, allowing non-prefix fuzzy omnifunc matches to appear in the PUM and be preinserted. Extend get_leader_for_startcol() to fall back to compl_orig_text when compl_leader.string is NULL: if the match's cpt source startcol is less than compl_col the match includes pre-compl_col text, so return &compl_leader (NULL string) to signal "pass through"; otherwise return &compl_orig_text so callers filter by the original text. The compl_col <= 0 guard is kept only for the prepend-text path to avoid it interfering with the NULL-leader fallback when compl_col is zero. With this change all callers of get_leader_for_startcol() automatically receive the correct filter string without additional helpers. Also update Test_autocomplete_trigger Test 9 to reflect the new behavior: 'faberge' is no longer shown when completing 'foo' because it does not start with the current prefix. Add Test_autocomplete_preinsert_null_leader() to verify that only prefix-matching candidates appear in the PUM and are preinserted. fixes: vim/vim#19328 closes: vim/vim#19447 https://github.com/vim/vim/commit/a04ae0210b18795d05d26609b6ba9fb9bf88caff Co-authored-by: Hirohito Higashi Co-Authored-By: Claude Sonnet 4.6 --- src/nvim/insexpand.c | 21 +++++++++-- test/old/testdir/test_ins_complete.vim | 48 ++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index f4e8f21b74..d7375fd9ee 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -1418,16 +1418,33 @@ static String *get_leader_for_startcol(compl_T *match, bool cached) return NULL; } - if (cpt_sources_array == NULL || compl_leader.data == NULL) { + if (cpt_sources_array == NULL) { goto theend; } int cpt_idx = match->cp_cpt_source_idx; - if (cpt_idx < 0 || compl_col <= 0) { + if (cpt_idx < 0) { goto theend; } int startcol = cpt_sources_array[cpt_idx].cs_startcol; + if (compl_leader.data == NULL) { + // When leader is not set (e.g. 'autocomplete' first fires before + // compl_leader is initialised), fall back to compl_orig_text for + // matches starting at or after compl_col. Matches starting before + // compl_col carry pre-compl_col text and must not be compared with + // compl_orig_text, so return &compl_leader (NULL string) to signal + // "pass through" (no prefix filter). + if (startcol < 0 || startcol >= compl_col) { + return &compl_orig_text; + } + return &compl_leader; // pass through (startcol < compl_col) + } + + if (compl_col <= 0) { + goto theend; + } + if (startcol >= 0 && startcol < compl_col) { int prepend_len = compl_col - startcol; int new_length = prepend_len + (int)compl_leader.size; diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 15e7964c45..68d0b9facb 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -5598,10 +5598,13 @@ func Test_autocomplete_trigger() call assert_equal(['fooze', 'faberge'], b:matches->mapnew('v:val.word')) " Test 9: Trigger autocomplete immediately upon entering Insert mode + " 'faberge' is filtered out because it doesn't start with the current prefix + " 'foo'; non-prefix omnifunc matches are excluded from the PUM when leader + " is NULL (compl_orig_text is used as a fallback filter). call feedkeys("Sprefix->foo\a\\0", 'tx!') - call assert_equal(['foobar', 'fooze', 'faberge'], b:matches->mapnew('v:val.word')) + call assert_equal(['foobar', 'fooze'], b:matches->mapnew('v:val.word')) call feedkeys("Sprefix->fooxx\hcw\\0", 'tx!') - call assert_equal(['foobar', 'fooze', 'faberge'], b:matches->mapnew('v:val.word')) + call assert_equal(['foobar', 'fooze'], b:matches->mapnew('v:val.word')) bw! call Ntest_override("char_avail", 0) @@ -6309,4 +6312,45 @@ func Test_helptags_autocomplete_timeout() bw! endfunc +func Test_autocomplete_preinsert_null_leader() + " Test that non-prefix matches from omnifunc are filtered when leader is NULL. + " When autocomplete first fires, compl_leader is NULL. Previously the prefix + " filter was bypassed, allowing non-prefix fuzzy matches to be incorrectly + " shown in the PUM and preinserted. + func NonPrefixOmni(findstart, base) + if a:findstart + return col(".") - 1 + endif + " Return "key" (doesn't start with 'y') and "yellow" (starts with 'y'). + " Simulates what a fuzzy omnifunc returns (e.g. vimcomplete#Complete with + " wildoptions=fuzzy). + return ["key", "yellow"] + endfunc + + call Ntest_override("char_avail", 1) + new + set omnifunc=NonPrefixOmni complete=o + set completeopt=preinsert autocomplete + + func GetState() + let g:line = getline('.') + let g:col = col('.') + let g:matches = complete_info(['matches']).matches->mapnew('v:val.word') + endfunc + inoremap =GetState() + + " Type 'y': "key" should be filtered out (doesn't start with 'y'), + " "yellow" should be the only PUM entry and preinserted with cursor after 'y'. + call feedkeys("iy\\\", 'tx') + call assert_equal("yellow", g:line) + call assert_equal(2, g:col) + call assert_equal(['yellow'], g:matches) + + bw! + set omnifunc& complete& completeopt& autocomplete& + call Ntest_override("char_avail", 0) + delfunc NonPrefixOmni + delfunc GetState +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable From 6e8a1a8988562757a979886e333034eb3eac70ea Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 20 Feb 2026 08:59:04 +0800 Subject: [PATCH 2/2] vim-patch:9.2.0032: completion: hang with line completion and fuzzy Problem: completion: hang with line completion and fuzzy (Jesse Pavel) Solution: Only check the line number when wrapping around the file (Hirohito Higashi). fixes: vim/vim#19434 closes: vim/vim#19443 https://github.com/vim/vim/commit/d8648f7279f5a61aaa9ceebc7330e09800947a35 Co-authored-by: Hirohito Higashi --- src/nvim/fuzzy.c | 3 ++- test/old/testdir/test_ins_complete.vim | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/nvim/fuzzy.c b/src/nvim/fuzzy.c index 9a9e95dda6..4d7f6a08e0 100644 --- a/src/nvim/fuzzy.c +++ b/src/nvim/fuzzy.c @@ -614,7 +614,8 @@ bool search_for_fuzzy_match(buf_T *buf, pos_T *pos, char *pattern, int dir, pos_ while (true) { // Check if looped around and back to start position - if (looped_around && equalpos(current_pos, circly_end)) { + if (looped_around && (whole_line ? current_pos.lnum == circly_end.lnum + : equalpos(current_pos, circly_end))) { break; } diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 68d0b9facb..f844f44a2c 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -3946,6 +3946,31 @@ func Test_complete_fuzzy_collect() set completeopt& cpt& ignorecase& infercase& endfunc +" Issue #19434 +" Fuzzy whole-line completion should not loop infinitely when the cursor is in +" the middle of the line (non-zero column). +func Test_complete_fuzzy_wholeline_no_hang() + new + set completeopt=preview,fuzzy,noinsert,menuone + call setline(1, [ + \ '', + \ '', + \ ' ', + \ ' ', + \ ' ', + \ '
', + \ '
', + \ ' ', + \ '', + \ ]) + call cursor(6, 1) + call feedkeys("faC\\\0", 'tx!') + call assert_equal('