diff --git a/src/insexpand.c b/src/insexpand.c index 2117427cf3..38e92dacfd 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -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; diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index 37553e6dbc..5d93a4248d 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -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\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 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 =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 test_override("char_avail", 0) + delfunc NonPrefixOmni + delfunc GetState +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable diff --git a/src/version.c b/src/version.c index 66e7169c3e..5df5e5dcc5 100644 --- a/src/version.c +++ b/src/version.c @@ -734,6 +734,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 30, /**/ 29, /**/