vim-patch:9.2.1001: complete_info() does not report the item highlight groups (#41461)

Problem:  complete_info() omits "abbr_hlgroup" and "kind_hlgroup".
Solution: Keep the highlight group ID with the match instead of the
          resolved attribute and add both entries to the returned items
          (glepnir).

closes: vim/vim#21105

1c32cede0a
This commit is contained in:
glepnir
2026-08-24 14:35:25 +08:00
committed by GitHub
parent 70958dae75
commit 92d5531f96
7 changed files with 108 additions and 54 deletions

View File

@@ -1247,13 +1247,15 @@ items:
|hl-PmenuMatchSel| and |hl-PmenuMatch| highlight
attributes in the popup menu to apply cterm and gui
properties (with higher priority) like strikethrough
to the completion items abbreviation
to the completion items abbreviation.
The group can be defined later.
kind_hlgroup an additional highlight group specifically for setting
the highlight attributes of the completion kind. When
this field is present, it will override the
|hl-PmenuKind| highlight group, allowing for the
customization of ctermfg and guifg properties for the
completion kind
completion kind.
The group can be defined later.
match See "matches" in |complete_info()|.
preselect when non-zero this item is selected by default in the
popup menu. Only if 'completeopt' has "preselect". If

View File

@@ -1320,10 +1320,13 @@ complete_info([{what}]) *complete_info()*
the currently selected index item.
items List of all completion candidates. Each item
is a dictionary containing the entries "word",
"abbr", "menu", "kind", "info" and
"user_data". "equal", "preselect" and
"commit_chars" are included only for items that
set them.
"abbr", "menu", "kind", "info",
"abbr_hlgroup", "kind_hlgroup" and
"user_data". The highlight group entries hold
the name that the item was added with,
or an empty string.
"equal", "preselect" and "commit_chars" are
included only for items that set them.
See |complete-items|.
matches Same as "items", but only returns items that
are matching current query. If both "matches"

View File

@@ -1139,10 +1139,13 @@ function vim.fn.complete_check() end
--- the currently selected index item.
--- items List of all completion candidates. Each item
--- is a dictionary containing the entries "word",
--- "abbr", "menu", "kind", "info" and
--- "user_data". "equal", "preselect" and
--- "commit_chars" are included only for items that
--- set them.
--- "abbr", "menu", "kind", "info",
--- "abbr_hlgroup", "kind_hlgroup" and
--- "user_data". The highlight group entries hold
--- the name that the item was added with,
--- or an empty string.
--- "equal", "preselect" and "commit_chars" are
--- included only for items that set them.
--- See |complete-items|.
--- matches Same as "items", but only returns items that
--- are matching current query. If both "matches"

View File

@@ -1493,10 +1493,13 @@ M.funcs = {
the currently selected index item.
items List of all completion candidates. Each item
is a dictionary containing the entries "word",
"abbr", "menu", "kind", "info" and
"user_data". "equal", "preselect" and
"commit_chars" are included only for items that
set them.
"abbr", "menu", "kind", "info",
"abbr_hlgroup", "kind_hlgroup" and
"user_data". The highlight group entries hold
the name that the item was added with,
or an empty string.
"equal", "preselect" and "commit_chars" are
included only for items that set them.
See |complete-items|.
matches Same as "items", but only returns items that
are matching current query. If both "matches"

View File

@@ -177,8 +177,8 @@ struct compl_S {
bool cp_preselect; ///< preselect item
int cp_score; ///< fuzzy match score or proximity score
bool cp_in_match_array; ///< collected by compl_match_array
int cp_user_abbr_hlattr; ///< highlight attribute for abbr
int cp_user_kind_hlattr; ///< highlight attribute for kind
int cp_user_abbr_hl_id; ///< highlight group ID for abbr
int cp_user_kind_hl_id; ///< highlight group ID for kind
int cp_cpt_source_idx; ///< index of this match's source in 'cpt' option
};
@@ -1075,8 +1075,8 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons
match->cp_fname = NULL;
}
match->cp_flags = flags;
match->cp_user_abbr_hlattr = user_hl ? user_hl[0] : -1;
match->cp_user_kind_hlattr = user_hl ? user_hl[1] : -1;
match->cp_user_abbr_hl_id = user_hl ? user_hl[0] : 0;
match->cp_user_kind_hl_id = user_hl ? user_hl[1] : 0;
match->cp_score = score;
match->cp_cpt_source_idx = cpt_sources_index;
@@ -1647,6 +1647,18 @@ static void sort_compl_match_list(MergeSortCompareFunc compare)
(void)ins_compl_make_cyclic();
}
/// Return the attribute for highlight group "hl_id", -1 when it has none.
static int get_user_highlight_attr(int hl_id)
{
int attr;
if (hl_id <= 0) {
return -1;
}
attr = syn_id2attr(hl_id);
return attr > 0 ? attr : -1;
}
/// Build a popup menu to show the completion matches.
///
/// @return the popup menu entry that should be selected,
@@ -1802,8 +1814,8 @@ 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_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_user_abbr_hlattr = get_user_highlight_attr(comp->cp_user_abbr_hl_id);
compl_match_array[i].pum_user_kind_hlattr = get_user_highlight_attr(comp->cp_user_kind_hl_id);
compl_match_array[i++].pum_extra = comp->cp_text[CPT_MENU] != NULL
? comp->cp_text[CPT_MENU] : comp->cp_fname;
compl_T *match_next = comp->cp_match_next;
@@ -3321,12 +3333,12 @@ theend:
}
}
static inline int get_user_highlight_attr(const char *hlname)
static inline int get_user_highlight_id(char *hlname)
{
if (hlname != NULL && *hlname != NUL) {
return syn_name2attr(hlname);
return syn_check_group(hlname, strlen(hlname));
}
return -1;
return 0;
}
/// Add a match to the list of matches from Vimscript object
@@ -3350,7 +3362,7 @@ static int ins_compl_add_tv(typval_T *const tv, const Direction dir, bool fast)
char *user_abbr_hlname = NULL;
char *user_kind_hlname = NULL;
char *commit_chars = NULL;
int user_hl[2] = { -1, -1 };
int user_hl[2] = { 0, 0 };
typval_T user_data;
user_data.v_type = VAR_UNKNOWN;
@@ -3363,10 +3375,10 @@ static int ins_compl_add_tv(typval_T *const tv, const Direction dir, bool fast)
commit_chars = tv_dict_get_string(tv->vval.v_dict, "commit_chars", true);
user_abbr_hlname = tv_dict_get_string(tv->vval.v_dict, "abbr_hlgroup", false);
user_hl[0] = get_user_highlight_attr(user_abbr_hlname);
user_hl[0] = get_user_highlight_id(user_abbr_hlname);
user_kind_hlname = tv_dict_get_string(tv->vval.v_dict, "kind_hlgroup", false);
user_hl[1] = get_user_highlight_attr(user_kind_hlname);
user_hl[1] = get_user_highlight_id(user_kind_hlname);
tv_dict_get_tv(tv->vval.v_dict, "user_data", &user_data);
@@ -3634,6 +3646,8 @@ static void fill_complete_info_dict(dict_T *di, compl_T *match, bool add_match)
tv_dict_add_str(di, S_LEN("menu"), match->cp_text[CPT_MENU]);
tv_dict_add_str(di, S_LEN("kind"), match->cp_text[CPT_KIND]);
tv_dict_add_str(di, S_LEN("info"), match->cp_text[CPT_INFO]);
tv_dict_add_str(di, S_LEN("abbr_hlgroup"), syn_id2name(match->cp_user_abbr_hl_id));
tv_dict_add_str(di, S_LEN("kind_hlgroup"), syn_id2name(match->cp_user_kind_hl_id));
if (add_match) {
tv_dict_add_bool(di, S_LEN("match"), match->cp_in_match_array);
}

View File

@@ -581,15 +581,15 @@ func Test_completefunc_info()
call assert_equal("matched{'preinserted_text': '', 'pum_visible': 0, 'mode': '', 'selected': -1, 'items': []}", getline(1))
%d
call feedkeys("i\<C-X>\<C-U>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
call assert_equal("matched{'preinserted_text': '', 'pum_visible': 1, 'mode': 'function', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
call assert_equal("matched{'preinserted_text': '', 'pum_visible': 1, 'mode': 'function', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
%d
set complete=.,FCompleteTest
call feedkeys("i\<C-N>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
call assert_equal("matched{'preinserted_text': '', 'pum_visible': 1, 'mode': 'keyword', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
call assert_equal("matched{'preinserted_text': '', 'pum_visible': 1, 'mode': 'keyword', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
%d
set complete=.,F
call feedkeys("i\<C-N>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
call assert_equal("matched{'preinserted_text': '', 'pum_visible': 1, 'mode': 'keyword', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
call assert_equal("matched{'preinserted_text': '', 'pum_visible': 1, 'mode': 'keyword', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
set completeopt&
set complete&
set completefunc&
@@ -700,10 +700,10 @@ func CompleteInfoTestUserDefinedFn(mvmt, idx, noselect)
set completeopt=menu,preview
endif
let items = "[" .
\ "{'word': 'foo', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'bar', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'baz', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'qux', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}" .
\ "{'word': 'foo', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'bar', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'baz', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'qux', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}" .
\ "]"
new
set completefunc=CompleteInfoUserDefinedFn
@@ -2043,7 +2043,7 @@ func Test_cpt_func_refresh_always_fail()
let g:CallCount = 0
exe "normal! Gof\<C-N>oo\<c-r>=complete_info([\"items\", \"selected\"])\<cr>"
call assert_equal('foo{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', '
\ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
\ . '''kind_hlgroup'': '''', ''abbr_hlgroup'': '''', ''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
\ getline(2))
call assert_equal(3, g:CallCount)
%d
@@ -2059,7 +2059,7 @@ func Test_cpt_func_refresh_always_fail()
let g:CallCount = 0
exe "normal! Gof\<C-N>o\<bs>\<c-r>=complete_info([\"items\", \"selected\"])\<cr>"
call assert_equal('f{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', '
\ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
\ . '''kind_hlgroup'': '''', ''abbr_hlgroup'': '''', ''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
\ getline(2))
call assert_equal(3, g:CallCount)
%d
@@ -2067,7 +2067,7 @@ func Test_cpt_func_refresh_always_fail()
let g:CallCount = 0
exe "normal! Gof\<C-N>oo\<bs>\<c-r>=complete_info([\"items\", \"selected\"])\<cr>"
call assert_equal('fo{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', '
\ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
\ . '''kind_hlgroup'': '''', ''abbr_hlgroup'': '''', ''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
\ getline(2))
call assert_equal(3, g:CallCount)
bw!
@@ -4254,26 +4254,26 @@ func Test_complete_info_matches()
call feedkeys("Go\<C-X>\<C-N>\<F5>\<Esc>dd", 'tx')
call assert_equal([
\ {'word': 'aaa', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aab', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aba', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'abb', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aaa', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aab', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aba', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'abb', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\], g:compl_info['matches'])
call feedkeys("Goa\<C-X>\<C-N>b\<F5>\<Esc>dd", 'tx')
call assert_equal([
\ {'word': 'aba', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'abb', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aba', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'abb', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\], g:compl_info['matches'])
" items and matches both in what
let g:what = ['items', 'matches']
call feedkeys("Goa\<C-X>\<C-N>b\<F5>\<Esc>dd", 'tx')
call assert_equal([
\ {'word': 'aaa', 'menu': '', 'user_data': '', 'match': v:false, 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aab', 'menu': '', 'user_data': '', 'match': v:false, 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aba', 'menu': '', 'user_data': '', 'match': v:true, 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'abb', 'menu': '', 'user_data': '', 'match': v:true, 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aaa', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'match': v:false, 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aab', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'match': v:false, 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'aba', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'match': v:true, 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'abb', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'match': v:true, 'info': '', 'kind': '', 'abbr': ''},
\], g:compl_info['items'])
call assert_false(has_key(g:compl_info, 'matches'))
@@ -4295,13 +4295,13 @@ func Test_complete_info_completed()
inoremap <buffer><F5> <C-R>=ShownInfo()<CR>
call feedkeys("Go\<C-X>\<C-N>\<F5>\<Esc>dd", 'tx')
call assert_equal({'word': 'aaa', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, g:compl_info['completed'])
call assert_equal({'word': 'aaa', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, g:compl_info['completed'])
call feedkeys("Go\<C-X>\<C-N>\<C-N>\<F5>\<Esc>dd", 'tx')
call assert_equal({'word': 'aab', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, g:compl_info['completed'])
call assert_equal({'word': 'aab', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, g:compl_info['completed'])
call feedkeys("Go\<C-X>\<C-N>\<C-N>\<C-N>\<C-N>\<F5>\<Esc>dd", 'tx')
call assert_equal({'word': 'abb', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, g:compl_info['completed'])
call assert_equal({'word': 'abb', 'menu': '', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, g:compl_info['completed'])
set completeopt+=noselect
call feedkeys("Go\<C-X>\<C-N>\<F5>\<Esc>dd", 'tx')
@@ -6822,4 +6822,33 @@ func Test_ins_complete_dedup()
unlet g:compl_info
endfunc
func Test_complete_info_hlgroup()
new
set completeopt&
func DoComplete()
call complete(1, [
\ #{word: 'aaa', abbr_hlgroup: 'Title', kind_hlgroup: 'SpecialKey'},
\ #{word: 'bbb', abbr_hlgroup: 'MyAbbr', kind_hlgroup: 'MyKind'},
\ #{word: 'ccc'}])
return ''
endfunc
func ShownItems()
let g:items = complete_info(['items']).items
return ''
endfunc
call assert_false(hlexists('MyAbbr'))
call feedkeys("i\<C-R>=DoComplete()\<CR>\<C-R>=ShownItems()\<CR>\<Esc>", 'tx')
call assert_equal(['Title', 'MyAbbr', ''],
\ map(copy(g:items), 'v:val.abbr_hlgroup'))
call assert_equal(['SpecialKey', 'MyKind', ''],
\ map(copy(g:items), 'v:val.kind_hlgroup'))
call assert_true(hlexists('MyAbbr'))
unlet g:items
delfunc DoComplete
delfunc ShownItems
bwipe!
endfunc
" vim: shiftwidth=2 sts=2 expandtab nofoldenable

View File

@@ -1150,11 +1150,11 @@ func Test_popup_complete_info_02()
\ 'mode': 'function',
\ 'pum_visible': 1,
\ 'items': [
\ {'word': 'Jan', 'menu': 'January', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'Feb', 'menu': 'February', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'Mar', 'menu': 'March', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'Apr', 'menu': 'April', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'May', 'menu': 'May', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}
\ {'word': 'Jan', 'menu': 'January', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'Feb', 'menu': 'February', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'Mar', 'menu': 'March', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'Apr', 'menu': 'April', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''},
\ {'word': 'May', 'menu': 'May', 'kind_hlgroup': '', 'abbr_hlgroup': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}
\ ],
\ 'preinserted_text': '',
\ 'selected': 0,