]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0030: completion: non-prefix matches shown when leader is NULL v9.2.0030
authorHirohito Higashi <h.east.727@gmail.com>
Thu, 19 Feb 2026 17:06:43 +0000 (17:06 +0000)
committerChristian Brabandt <cb@256bit.org>
Thu, 19 Feb 2026 17:06:43 +0000 (17:06 +0000)
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:  #19328
closes: #19447

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
src/insexpand.c
src/testdir/test_ins_complete.vim
src/version.c

index 2117427cf3b29438fb0c4afab97e144164b1b1a7..38e92dacfd625973233f6da27007f60a98cc1b7d 100644 (file)
@@ -1520,14 +1520,30 @@ get_leader_for_startcol(compl_T *match, int cached)
        return NULL;
     }
 
-    if (cpt_sources_array == NULL || compl_leader.string == 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.string == 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;
index 37553e6dbcc74f0abbd2304648c4e0b8659d8eb6..5d93a4248d77fb5d8270917471c77640e9d493c2 100644 (file)
@@ -5486,10 +5486,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\<Esc>a\<F2>\<Esc>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\<Esc>hcw\<F2>\<Esc>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 test_override("char_avail", 0)
@@ -6196,4 +6199,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 test_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 <buffer> <F5> <C-R>=GetState()<CR>
+
+  " 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\<F5>\<C-E>\<Esc>", '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 test_override("char_avail", 0)
+  delfunc NonPrefixOmni
+  delfunc GetState
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab nofoldenable
index 66e7169c3e469f5c4e5fc373a6a0967a9fc8e165..5df5e5dcc59999be2f2c08c2ae9f55536ea5e94a 100644 (file)
@@ -734,6 +734,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    30,
 /**/
     29,
 /**/