patch 9.2.0433: customlist completion cannot supply pum metadata

Problem:  customlist completion cannot supply pum metadata
Solution: Allow each item returned by a customlist function to be
          either a string or a Dict with keys "word", "abbr", "kind",
          "menu" and "info" (Yasuhiro Matsumoto).

closes: #20100

Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Yasuhiro Matsumoto
2026-05-02 16:04:38 +00:00
committed by Christian Brabandt
parent 3bd25c63b4
commit 5c700152ae
6 changed files with 195 additions and 16 deletions
+16 -2
View File
@@ -1,4 +1,4 @@
*map.txt* For Vim version 9.2. Last change: 2026 Feb 14
*map.txt* For Vim version 9.2. Last change: 2026 May 02
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -1693,7 +1693,21 @@ For the "custom" argument, the function should return the completion
candidates one per line in a newline separated string.
*E1303*
For the "customlist" argument, the function should return the completion
candidates as a Vim List. Non-string items in the list are ignored.
candidates as a Vim List. Each item may be either a string or a |Dictionary|.
A Dictionary item may have the following keys:
word (required) the text inserted into the command line when the
item is selected
abbr alternative text shown in the popup menu in place of "word",
when 'wildoptions' contains "pum"; useful when the inserted
text and the displayed text should differ
kind short kind text (one or two characters), shown in the popup
menu when 'wildoptions' contains "pum"
menu extra text shown after the match in the popup menu
info long description shown in the info popup; the |+popupwin|
feature is required to display it
Items that are neither a string nor a Dictionary, and Dictionary items without
a "word" key, are ignored. When 'wildoptions' does not contain "pum", only
"word" is shown.
The function arguments are:
ArgLead the leading portion of the argument currently being
+3 -1
View File
@@ -1,4 +1,4 @@
*version9.txt* For Vim version 9.2. Last change: 2026 May 01
*version9.txt* For Vim version 9.2. Last change: 2026 May 02
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52623,6 +52623,8 @@ Other ~
'completeopt' option
- Channel can handle |Blob| messages |channel-open-options|.
- Added the "u" flag to 'shortmess' to silence undo/redo messages: |shm-u|
- |:command-completion-customlist| can return a list of dictionaries with
kind/menu/info/abbr for the popup menu.
Platform specific ~
-----------------
+116 -13
View File
@@ -412,10 +412,16 @@ cmdline_pum_create(
compl_match_arraysize = numMatches;
for (int i = 0; i < numMatches; i++)
{
compl_match_array[i].pum_text = SHOW_MATCH(i);
compl_match_array[i].pum_info = NULL;
compl_match_array[i].pum_extra = NULL;
compl_match_array[i].pum_kind = NULL;
compl_match_array[i].pum_text = (xp->xp_files_abbr != NULL
&& xp->xp_files_abbr[i] != NULL)
? xp->xp_files_abbr[i]
: SHOW_MATCH(i);
compl_match_array[i].pum_info = xp->xp_files_info != NULL
? xp->xp_files_info[i] : NULL;
compl_match_array[i].pum_extra = xp->xp_files_menu != NULL
? xp->xp_files_menu[i] : NULL;
compl_match_array[i].pum_kind = xp->xp_files_kind != NULL
? xp->xp_files_kind[i] : NULL;
compl_match_array[i].pum_user_abbr_hlattr = -1;
compl_match_array[i].pum_user_kind_hlattr = -1;
}
@@ -1021,6 +1027,31 @@ find_longest_match(expand_T *xp, int options)
return ss;
}
static void
free_xp_files_extra(expand_T *xp, int numfiles)
{
if (xp->xp_files_abbr != NULL)
{
FreeWild(numfiles, xp->xp_files_abbr);
xp->xp_files_abbr = NULL;
}
if (xp->xp_files_kind != NULL)
{
FreeWild(numfiles, xp->xp_files_kind);
xp->xp_files_kind = NULL;
}
if (xp->xp_files_menu != NULL)
{
FreeWild(numfiles, xp->xp_files_menu);
xp->xp_files_menu = NULL;
}
if (xp->xp_files_info != NULL)
{
FreeWild(numfiles, xp->xp_files_info);
xp->xp_files_info = NULL;
}
}
/*
* Do wildcard expansion on the string "str".
* Chars that should not be expanded must be preceded with a backslash.
@@ -1087,6 +1118,7 @@ ExpandOne(
if (xp->xp_numfiles != -1 && mode != WILD_ALL && mode != WILD_LONGEST)
{
FreeWild(xp->xp_numfiles, xp->xp_files);
free_xp_files_extra(xp, xp->xp_numfiles);
xp->xp_numfiles = -1;
VIM_CLEAR(xp->xp_orig);
@@ -1188,6 +1220,7 @@ ExpandCleanup(expand_T *xp)
{
if (xp->xp_numfiles >= 0)
{
free_xp_files_extra(xp, xp->xp_numfiles);
FreeWild(xp->xp_numfiles, xp->xp_files);
xp->xp_numfiles = -1;
}
@@ -1424,7 +1457,10 @@ showmatches(
}
if (xp->xp_numfiles == -1)
{
FreeWild(numMatches, matches);
free_xp_files_extra(xp, numMatches);
}
return EXPAND_OK;
}
@@ -4124,6 +4160,12 @@ ExpandUserList(
list_T *retlist;
listitem_T *li;
garray_T ga;
garray_T ga_abbr;
garray_T ga_kind;
garray_T ga_menu;
garray_T ga_info;
int have_extra = FALSE;
int i;
*matches = NULL;
*numMatches = 0;
@@ -4132,31 +4174,92 @@ ExpandUserList(
return FAIL;
ga_init2(&ga, sizeof(char *), 3);
ga_init2(&ga_abbr, sizeof(char *), 3);
ga_init2(&ga_kind, sizeof(char *), 3);
ga_init2(&ga_menu, sizeof(char *), 3);
ga_init2(&ga_info, sizeof(char *), 3);
// Loop over the items in the list.
FOR_ALL_LIST_ITEMS(retlist, li)
{
char_u *p;
char_u *p = NULL;
char_u *abbr = NULL;
char_u *kind = NULL;
char_u *menu = NULL;
char_u *info = NULL;
if (li->li_tv.v_type != VAR_STRING || li->li_tv.vval.v_string == NULL)
continue; // Skip non-string items and empty strings
if (li->li_tv.v_type == VAR_STRING)
{
if (li->li_tv.vval.v_string == NULL)
continue; // Skip empty strings
p = vim_strsave(li->li_tv.vval.v_string);
}
else if (li->li_tv.v_type == VAR_DICT
&& li->li_tv.vval.v_dict != NULL)
{
dict_T *d = li->li_tv.vval.v_dict;
char_u *word = dict_get_string(d, "word", FALSE);
p = vim_strsave(li->li_tv.vval.v_string);
if (p == NULL)
break;
if (word == NULL)
continue; // "word" is required
p = vim_strsave(word);
abbr = dict_get_string(d, "abbr", TRUE);
kind = dict_get_string(d, "kind", TRUE);
menu = dict_get_string(d, "menu", TRUE);
info = dict_get_string(d, "info", TRUE);
if (abbr != NULL || kind != NULL || menu != NULL || info != NULL)
have_extra = TRUE;
}
else
continue; // Skip other types
if (ga_grow(&ga, 1) == FAIL)
if (p == NULL
|| ga_grow(&ga, 1) == FAIL
|| ga_grow(&ga_abbr, 1) == FAIL
|| ga_grow(&ga_kind, 1) == FAIL
|| ga_grow(&ga_menu, 1) == FAIL
|| ga_grow(&ga_info, 1) == FAIL)
{
vim_free(p);
vim_free(abbr);
vim_free(kind);
vim_free(menu);
vim_free(info);
break;
}
((char_u **)ga.ga_data)[ga.ga_len] = p;
++ga.ga_len;
((char_u **)ga.ga_data)[ga.ga_len++] = p;
((char_u **)ga_abbr.ga_data)[ga_abbr.ga_len++] = abbr;
((char_u **)ga_kind.ga_data)[ga_kind.ga_len++] = kind;
((char_u **)ga_menu.ga_data)[ga_menu.ga_len++] = menu;
((char_u **)ga_info.ga_data)[ga_info.ga_len++] = info;
}
list_unref(retlist);
*matches = ga.ga_data;
*numMatches = ga.ga_len;
if (have_extra && ga.ga_len > 0)
{
xp->xp_files_abbr = (char_u **)ga_abbr.ga_data;
xp->xp_files_kind = (char_u **)ga_kind.ga_data;
xp->xp_files_menu = (char_u **)ga_menu.ga_data;
xp->xp_files_info = (char_u **)ga_info.ga_data;
}
else
{
// No extra info collected; free the placeholder NULL entries.
for (i = 0; i < ga_abbr.ga_len; i++)
vim_free(((char_u **)ga_abbr.ga_data)[i]);
vim_free(ga_abbr.ga_data);
for (i = 0; i < ga_kind.ga_len; i++)
vim_free(((char_u **)ga_kind.ga_data)[i]);
vim_free(ga_kind.ga_data);
for (i = 0; i < ga_menu.ga_len; i++)
vim_free(((char_u **)ga_menu.ga_data)[i]);
vim_free(ga_menu.ga_data);
for (i = 0; i < ga_info.ga_len; i++)
vim_free(((char_u **)ga_info.ga_data)[i]);
vim_free(ga_info.ga_data);
}
return OK;
}
#endif
+11
View File
@@ -678,6 +678,17 @@ typedef struct expand
int xp_selected; // selected index in completion
char_u *xp_orig; // originally expanded string
char_u **xp_files; // list of files
char_u **xp_files_abbr; // optional parallel array of display
// strings (override xp_files for the
// pum text); NULL if unused
char_u **xp_files_kind; // optional parallel array of "kind"
// strings; NULL if unused
char_u **xp_files_menu; // optional parallel array of "menu"
// strings (shown after the match);
// NULL if unused
char_u **xp_files_info; // optional parallel array of "info"
// strings (shown in info popup);
// NULL if unused
char_u *xp_line; // text being completed
#define EXPAND_BUF_LEN 256
char_u xp_buf[EXPAND_BUF_LEN]; // buffer for returned match
+47
View File
@@ -4594,6 +4594,53 @@ func Test_custom_completion()
delfunc Check_customlist_completion
endfunc
" Test that 'customlist' completion accepts dict items with extra info
" (kind/menu/info) for display in the popup menu, and that string items still
" work in the same list.
func Test_customlist_dict_completion()
func DictComp(A, L, P)
return [
\ {'word': 'apple', 'kind': 'f', 'menu': 'fruit', 'info': 'A red fruit'},
\ {'word': 'banana', 'kind': 'f', 'menu': 'fruit', 'info': 'A yellow fruit'},
\ {'word': 'carrot', 'kind': 'v', 'menu': 'vegetable', 'info': 'An orange vegetable'},
\ 'plain',
\ ]
endfunc
command -nargs=1 -complete=customlist,DictComp DictCmd echo <q-args>
" getcompletion() returns only the "word" of each item; string items pass
" through unchanged.
call assert_equal(['apple', 'banana', 'carrot', 'plain'],
\ getcompletion('', 'customlist,DictComp'))
" Items missing a "word" key are silently skipped.
func DictCompMissingWord(A, L, P)
return [{'kind': 'x'}, {'word': 'ok'}]
endfunc
call assert_equal(['ok'],
\ getcompletion('', 'customlist,DictCompMissingWord'))
" Tab completion still selects the word.
call feedkeys(":DictCmd a\<Tab>\<C-B>\"\<CR>", 'xt')
call assert_equal('"DictCmd apple', @:)
" "abbr" overrides display only; "word" is what gets inserted.
func DictCompAbbr(A, L, P)
return [{'word': 'apple', 'abbr': 'APPLE🍎'}]
endfunc
call assert_equal(['apple'],
\ getcompletion('', 'customlist,DictCompAbbr'))
command -nargs=1 -complete=customlist,DictCompAbbr DictAbbrCmd echo <q-args>
call feedkeys(":DictAbbrCmd \<Tab>\<C-B>\"\<CR>", 'xt')
call assert_equal('"DictAbbrCmd apple', @:)
delcommand DictAbbrCmd
delcommand DictCmd
delfunc DictComp
delfunc DictCompMissingWord
delfunc DictCompAbbr
endfunc
func Test_custom_completion_with_glob()
func TestGlobComplete(A, L, P)
return split(glob('Xglob*'), "\n")
+2
View File
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
433,
/**/
432,
/**/