neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit bec1449cc51081e064d254f88601358094f3abe8
parent 2411f924a320f3be79abce1845e3b1fa438a9572
Author: zeertzjq <zeertzjq@outlook.com>
Date:   Sat, 31 May 2025 19:15:54 +0800

vim-patch:9.1.1416: completion limits not respected for fuzzy completions

Problem:  completion limits not respected when using fuzzy completion
          (Maxim Kim)
Solution: trim completion array (Girish Palya)

fixes: vim/vim#17379
closes: vim/vim#17386

https://github.com/vim/vim/commit/19ef6b0b4b11a9775f9c90edc68c896034fd2a9d

Co-authored-by: Girish Palya <girishji@gmail.com>

Diffstat:
Msrc/nvim/insexpand.c | 62+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/nvim/popupmenu.h | 1+
Mtest/old/testdir/test_ins_complete.vim | 46++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 104 insertions(+), 5 deletions(-)

diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c @@ -1362,6 +1362,58 @@ static void trigger_complete_changed_event(int cur) restore_v_event(v_event, &save_v_event); } +/// Trim compl_match_array to enforce max_matches per completion source. +/// +/// Note: This special-case trimming is a workaround because compl_match_array +/// becomes inconsistent with compl_first_match (list) after former is sorted by +/// fuzzy score. The two structures end up in different orders. +/// Ideally, compl_first_match list should have been sorted instead. +static void trim_compl_match_array(void) +{ + // Count current matches per source. + int *match_counts = xcalloc((size_t)cpt_sources_count, sizeof(int)); + for (int i = 0; i < compl_match_arraysize; i++) { + int src_idx = compl_match_array[i].pum_cpt_source_idx; + if (src_idx != -1) { + match_counts[src_idx]++; + } + } + + // Calculate size of trimmed array, respecting max_matches per source. + int new_size = 0; + for (int i = 0; i < cpt_sources_count; i++) { + int limit = cpt_sources_array[i].max_matches; + new_size += (limit > 0 && match_counts[i] > limit) ? limit : match_counts[i]; + } + + if (new_size == compl_match_arraysize) { + goto theend; + } + + // Create trimmed array while enforcing per-source limits + pumitem_T *trimmed = xcalloc((size_t)new_size, sizeof(pumitem_T)); + memset(match_counts, 0, sizeof(int) * (size_t)cpt_sources_count); + int trimmed_idx = 0; + for (int i = 0; i < compl_match_arraysize; i++) { + int src_idx = compl_match_array[i].pum_cpt_source_idx; + if (src_idx != -1) { + int limit = cpt_sources_array[src_idx].max_matches; + if (limit <= 0 || match_counts[src_idx] < limit) { + trimmed[trimmed_idx++] = compl_match_array[i]; + match_counts[src_idx]++; + } + } else { + trimmed[trimmed_idx++] = compl_match_array[i]; + } + } + xfree(compl_match_array); + compl_match_array = trimmed; + compl_match_arraysize = new_size; + +theend: + xfree(match_counts); +} + /// pumitem qsort compare func static int ins_compl_fuzzy_cmp(const void *a, const void *b) { @@ -1398,7 +1450,7 @@ static int ins_compl_build_pum(void) int match_count = 0; int cur_source = -1; bool max_matches_found = false; - bool is_forward = compl_shows_dir_forward() && !fuzzy_filter; + bool is_forward = compl_shows_dir_forward(); // If the current match is the original text don't find the first // match after it, don't highlight anything. @@ -1433,7 +1485,7 @@ static int ins_compl_build_pum(void) comp->cp_flags &= ~CP_ICASE; } - if (is_forward && comp->cp_cpt_source_idx != -1) { + if (is_forward && !fuzzy_sort && comp->cp_cpt_source_idx != -1) { if (cur_source != comp->cp_cpt_source_idx) { cur_source = comp->cp_cpt_source_idx; match_count = 1; @@ -1490,7 +1542,7 @@ static int ins_compl_build_pum(void) shown_match_ok = true; } } - if (is_forward && comp->cp_cpt_source_idx != -1) { + if (is_forward && !fuzzy_sort && comp->cp_cpt_source_idx != -1) { match_count++; } i++; @@ -1534,6 +1586,7 @@ static int ins_compl_build_pum(void) compl_match_array[i].pum_kind = comp->cp_text[CPT_KIND]; compl_match_array[i].pum_info = comp->cp_text[CPT_INFO]; compl_match_array[i].pum_score = comp->cp_score; + compl_match_array[i].pum_cpt_source_idx = comp->cp_cpt_source_idx; compl_match_array[i].pum_user_abbr_hlattr = comp->cp_user_abbr_hlattr; compl_match_array[i].pum_user_kind_hlattr = comp->cp_user_kind_hlattr; compl_match_array[i++].pum_extra = comp->cp_text[CPT_MENU] != NULL @@ -1553,6 +1606,9 @@ static int ins_compl_build_pum(void) shown_match_ok = true; } + if (is_forward && fuzzy_sort && cpt_sources_array != NULL) { + trim_compl_match_array(); // Truncate by max_matches in 'cpt' + } if (!shown_match_ok) { // no displayed match at all cur = -1; } diff --git a/src/nvim/popupmenu.h b/src/nvim/popupmenu.h @@ -16,6 +16,7 @@ typedef struct { char *pum_info; ///< extra info int pum_score; ///< fuzzy match score int pum_idx; ///< index of item before sorting by score + int pum_cpt_source_idx; ///< index of completion source in 'cpt' int pum_user_abbr_hlattr; ///< highlight attribute for abbr int pum_user_kind_hlattr; ///< highlight attribute for kind } pumitem_T; diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim @@ -4159,7 +4159,7 @@ func Test_complete_multiline_marks() endfunc func Test_complete_match_count() - func PrintMenuWords() + func! PrintMenuWords() let info = complete_info(["selected", "matches"]) call map(info.matches, {_, v -> v.word}) return info @@ -4279,8 +4279,50 @@ func Test_complete_match_count() call assert_equal(3, g:CallCount) bw! + " Test 'fuzzy' with max_items + " XXX: Cannot use complete_info() since it is broken for 'fuzzy' + new + set completeopt=menu,noselect,fuzzy + set complete=. + call setline(1, ["abcd", "abac", "abdc"]) + execute "normal Goa\<c-n>c\<c-n>" + call assert_equal('abac', getline(4)) + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^1 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^2 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^3 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^4 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + + func! ComplFunc(findstart, base) + if a:findstart + return col(".") + endif + return ["abcde", "abacr"] + endfunc + + set complete=.,FComplFunc^1 + execute "normal Sa\<c-n>c\<c-n>\<c-n>" + call assert_equal('abacr', getline(4)) + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^1,FComplFunc^1 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + bw! + set completeopt& complete& delfunc PrintMenuWords + delfunc ComplFunc + delfunc CompleteItemsSelect endfunc func Test_complete_append_selected_match_default() @@ -4402,7 +4444,7 @@ endfunc " Test 'nearest' flag of 'completeopt' func Test_nearest_cpt_option() - func PrintMenuWords() + func! PrintMenuWords() let info = complete_info(["selected", "matches"]) call map(info.matches, {_, v -> v.word}) return info