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:
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