Commit: patch 9.2.1001: complete_info() does not report the item highlight groups

2 views
Skip to first unread message

Christian Brabandt

unread,
Aug 23, 2026, 4:15:17 PM (2 days ago) Aug 23
to vim...@googlegroups.com
patch 9.2.1001: complete_info() does not report the item highlight groups

Commit: https://github.com/vim/vim/commit/1c32cede0afdc9351d5887e2f25977d4fb964d9f
Author: glepnir <gleph...@gmail.com>
Date: Sun Aug 23 19:58:27 2026 +0000

patch 9.2.1001: complete_info() does not report the item highlight groups

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

Signed-off-by: glepnir <gleph...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index f38a06657..c37b7b0d0 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -2063,8 +2063,10 @@ 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".
+ "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.
See |complete-items|.
matches Same as "items", but only returns items that
are matching current query. If both "matches"
diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt
index f581d50cf..18408dfe8 100644
--- a/runtime/doc/insert.txt
+++ b/runtime/doc/insert.txt
@@ -1,4 +1,4 @@
-*insert.txt* For Vim version 9.2. Last change: 2026 Jul 22
+*insert.txt* For Vim version 9.2. Last change: 2026 Aug 23


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -1264,13 +1264,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()|.

All of these except "icase", "equal", "dup" and "empty" must be a string. If
diff --git a/src/insexpand.c b/src/insexpand.c
index 203ea6fc3..f63f4ff6b 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -110,8 +110,8 @@ struct compl_S
int cp_number; // sequence number
int cp_score; // fuzzy match score or proximity score
int 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
};

@@ -1047,8 +1047,8 @@ ins_compl_add(
else
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;

@@ -1742,6 +1742,20 @@ sort_compl_match_list(int (*compare)(const void *, const void *))
(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.
* Returns the popup menu entry that should be selected. Returns -1 if nothing
@@ -1905,8 +1919,10 @@ ins_compl_build_pum(void)
compl_match_array[i].pum_kind = compl->cp_text[CPT_KIND];
compl_match_array[i].pum_info = compl->cp_text[CPT_INFO];
compl_match_array[i].pum_cpt_source_idx = compl->cp_cpt_source_idx;
- compl_match_array[i].pum_user_abbr_hlattr = compl->cp_user_abbr_hlattr;
- compl_match_array[i].pum_user_kind_hlattr = compl->cp_user_kind_hlattr;
+ compl_match_array[i].pum_user_abbr_hlattr =
+ get_user_highlight_attr(compl->cp_user_abbr_hl_id);
+ compl_match_array[i].pum_user_kind_hlattr =
+ get_user_highlight_attr(compl->cp_user_kind_hl_id);
compl_match_array[i++].pum_extra = compl->cp_text[CPT_MENU] != NULL
? compl->cp_text[CPT_MENU] : compl->cp_fname;
match_next = compl->cp_match_next;
@@ -3850,11 +3866,11 @@ theend:
#if defined(FEAT_COMPL_FUNC) || defined(FEAT_EVAL)

static inline int
-get_user_highlight_attr(char_u *hlname)
+get_user_highlight_id(char_u *hlname)
{
if (hlname != NULL && *hlname != NUL)
- return syn_name2attr(hlname);
- return -1;
+ return syn_check_group(hlname, STRLEN(hlname));
+ return 0;
}
/*
* Add a match to the list of matches from a typeval_T.
@@ -3875,7 +3891,7 @@ ins_compl_add_tv(typval_T *tv, int dir, int fast)
int status;
char_u *user_abbr_hlname;
char_u *user_kind_hlname;
- int user_hl[2] = { -1, -1 };
+ int user_hl[2] = { 0, 0 };

user_data.v_type = VAR_UNKNOWN;
if (tv->v_type == VAR_DICT && tv->vval.v_dict != NULL)
@@ -3887,10 +3903,10 @@ ins_compl_add_tv(typval_T *tv, int dir, int fast)
cptext[CPT_INFO] = dict_get_string(tv->vval.v_dict, "info", FALSE);

user_abbr_hlname = 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 = 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);

dict_get_tv(tv->vval.v_dict, "user_data", &user_data);
if (dict_get_string(tv->vval.v_dict, "icase", FALSE) != NULL
@@ -4184,6 +4200,8 @@ fill_complete_info_dict(dict_T *di, compl_T *match, int add_match)
dict_add_string(di, "menu", match->cp_text[CPT_MENU]);
dict_add_string(di, "kind", match->cp_text[CPT_KIND]);
dict_add_string(di, "info", match->cp_text[CPT_INFO]);
+ dict_add_string(di, "abbr_hlgroup", syn_id2name(match->cp_user_abbr_hl_id));
+ dict_add_string(di, "kind_hlgroup", syn_id2name(match->cp_user_kind_hl_id));
if (add_match)
dict_add_bool(di, "match", match->cp_in_match_array);
if (match->cp_user_data.v_type == VAR_UNKNOWN)
diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index a0ee08dc1..b83e605aa 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -576,15 +576,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&
@@ -693,10 +693,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
@@ -1955,7 +1955,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
@@ -1971,7 +1971,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
@@ -1979,7 +1979,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!
@@ -4162,26 +4162,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'))

@@ -4203,13 +4203,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')
@@ -6727,4 +6727,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
diff --git a/src/testdir/test_popup.vim b/src/testdir/test_popup.vim
index f5f2f1a7b..a1d11b55f 100644
--- a/src/testdir/test_popup.vim
+++ b/src/testdir/test_popup.vim
@@ -1161,11 +1161,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,
diff --git a/src/version.c b/src/version.c
index 92fdb20c6..586d940fd 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 1001,
/**/
1000,
/**/
Reply all
Reply to author
Forward
0 new messages