From 92d5531f96fef519c4427b369aa5cd2bbff44cf6 Mon Sep 17 00:00:00 2001 From: glepnir Date: Mon, 24 Aug 2026 14:35:25 +0800 Subject: [PATCH] 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 https://github.com/vim/vim/commit/1c32cede0afdc9351d5887e2f25977d4fb964d9f --- runtime/doc/insert.txt | 6 ++- runtime/doc/vimfn.txt | 11 ++-- runtime/lua/vim/_meta/vimfn.gen.lua | 11 ++-- src/nvim/eval.lua | 11 ++-- src/nvim/insexpand.c | 38 ++++++++----- test/old/testdir/test_ins_complete.vim | 75 ++++++++++++++++++-------- test/old/testdir/test_popup.vim | 10 ++-- 7 files changed, 108 insertions(+), 54 deletions(-) diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index 9377c3c1e0..5b5febe2bf 100644 --- a/runtime/doc/insert.txt +++ b/runtime/doc/insert.txt @@ -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 diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index 19e3e4948b..d7efd564c8 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -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" diff --git a/runtime/lua/vim/_meta/vimfn.gen.lua b/runtime/lua/vim/_meta/vimfn.gen.lua index 8109219a3e..d9b07b7bc9 100644 --- a/runtime/lua/vim/_meta/vimfn.gen.lua +++ b/runtime/lua/vim/_meta/vimfn.gen.lua @@ -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" diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 04801e8a2d..d034a38ca1 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -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" diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 642e995098..af8f4e8940 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -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); } diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index e0fbbb6fea..a597419f1b 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -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\\\\=string(complete_info())\\", "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\\\=string(complete_info())\\", "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\\\=string(complete_info())\\", "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\oo\=complete_info([\"items\", \"selected\"])\" 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\o\\=complete_info([\"items\", \"selected\"])\" 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\oo\\=complete_info([\"items\", \"selected\"])\" 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\\\\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\\b\\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\\b\\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 =ShownInfo() call feedkeys("Go\\\\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\\\\\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\\\\\\\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\\\\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\=DoComplete()\\=ShownItems()\\", '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 diff --git a/test/old/testdir/test_popup.vim b/test/old/testdir/test_popup.vim index 9fe110dd9c..5ec733b9de 100644 --- a/test/old/testdir/test_popup.vim +++ b/test/old/testdir/test_popup.vim @@ -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,