vim-patch:9.2.0433: customlist completion cannot supply pum metadata (#39584)

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: vim/vim#20100

5c700152ae

Co-authored-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
This commit is contained in:
zeertzjq
2026-05-04 11:47:54 +08:00
committed by GitHub
parent cdccd452ff
commit 7fff439395
6 changed files with 178 additions and 13 deletions

View File

@@ -1448,7 +1448,20 @@ For the "custom" argument, the function should return the completion
candidates one per line in a newline separated string.
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
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

View File

@@ -133,6 +133,8 @@ DIAGNOSTICS
EDITOR
• |:command-completion-customlist| can return a list of dictionaries with
kind/menu/info/abbr for the popup menu.
• |gf| and |<cfile>| support `file://…` URIs.
• |:log| opens log files.
• |ZR| restarts Nvim (|:restart|).

View File

@@ -394,10 +394,11 @@ static void cmdline_pum_create(CmdlineInfo *ccline, expand_T *xp, char **matches
compl_match_arraysize = numMatches;
for (int i = 0; i < numMatches; i++) {
compl_match_array[i] = (pumitem_T){
.pum_text = SHOW_MATCH(i),
.pum_info = NULL,
.pum_extra = NULL,
.pum_kind = NULL,
.pum_text = (xp->xp_files_abbr != NULL && xp->xp_files_abbr[i] != NULL)
? xp->xp_files_abbr[i] : SHOW_MATCH(i),
.pum_info = xp->xp_files_info != NULL ? xp->xp_files_info[i] : NULL,
.pum_extra = xp->xp_files_menu != NULL ? xp->xp_files_menu[i] : NULL,
.pum_kind = xp->xp_files_kind != NULL ? xp->xp_files_kind[i] : NULL,
.pum_user_abbr_hlattr = -1,
.pum_user_kind_hlattr = -1,
};
@@ -872,6 +873,26 @@ static char *find_longest_match(expand_T *xp, int options)
return xmemdupz(xp->xp_files[0], len);
}
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.
/// Return a pointer to allocated memory containing the new string.
@@ -934,6 +955,7 @@ char *ExpandOne(expand_T *xp, char *str, char *orig, int options, int mode)
// free old names
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;
XFREE_CLEAR(xp->xp_orig);
@@ -1024,6 +1046,7 @@ void ExpandInit(expand_T *xp)
void 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;
}
@@ -1211,6 +1234,7 @@ int showmatches(expand_T *xp, bool display_wildmenu, bool display_list, int wim_
if (xp->xp_numfiles == -1) {
FreeWild(numMatches, matches);
free_xp_files_extra(xp, numMatches);
}
return EXPAND_OK;
@@ -3536,24 +3560,86 @@ static int ExpandUserDefined(const char *const pat, expand_T *xp, regmatch_T *re
return OK;
}
static void process_user_list(list_T *retlist, char ***matches, int *numMatches)
static void process_user_list(list_T *retlist, char ***matches, int *numMatches, expand_T *xp)
{
garray_T ga;
ga_init(&ga, (int)sizeof(char *), 3);
garray_T ga_abbr;
garray_T ga_kind;
garray_T ga_menu;
garray_T ga_info;
bool have_extra = false;
ga_init(&ga, sizeof(char *), 3);
ga_init(&ga_abbr, sizeof(char *), 3);
ga_init(&ga_kind, sizeof(char *), 3);
ga_init(&ga_menu, sizeof(char *), 3);
ga_init(&ga_info, sizeof(char *), 3);
// Loop over the items in the list.
TV_LIST_ITER_CONST(retlist, li, {
if (TV_LIST_ITEM_TV(li)->v_type != VAR_STRING
|| TV_LIST_ITEM_TV(li)->vval.v_string == NULL) {
continue; // Skip non-string items and empty strings.
char *p = NULL;
char *abbr = NULL;
char *kind = NULL;
char *menu = NULL;
char *info = NULL;
if (TV_LIST_ITEM_TV(li)->v_type == VAR_STRING) {
if (TV_LIST_ITEM_TV(li)->vval.v_string == NULL) {
continue; // Skip empty strings
}
p = xstrdup(TV_LIST_ITEM_TV(li)->vval.v_string);
} else if (TV_LIST_ITEM_TV(li)->v_type == VAR_DICT
&& TV_LIST_ITEM_TV(li)->vval.v_dict != NULL) {
dict_T *d = TV_LIST_ITEM_TV(li)->vval.v_dict;
char *word = tv_dict_get_string(d, "word", false);
if (word == NULL) {
continue; // "word" is required
}
p = xstrdup(word);
abbr = tv_dict_get_string(d, "abbr", true);
kind = tv_dict_get_string(d, "kind", true);
menu = tv_dict_get_string(d, "menu", true);
info = tv_dict_get_string(d, "info", true);
if (abbr != NULL || kind != NULL || menu != NULL || info != NULL) {
have_extra = true;
}
} else {
continue; // Skip other types
}
char *p = xstrdup(TV_LIST_ITEM_TV(li)->vval.v_string);
GA_APPEND(char *, &ga, p);
GA_APPEND(char *, &ga_abbr, abbr);
GA_APPEND(char *, &ga_kind, kind);
GA_APPEND(char *, &ga_menu, menu);
GA_APPEND(char *, &ga_info, info);
});
tv_list_unref(retlist);
*matches = ga.ga_data;
*numMatches = ga.ga_len;
if (have_extra && ga.ga_len > 0) {
xp->xp_files_abbr = (char **)ga_abbr.ga_data;
xp->xp_files_kind = (char **)ga_kind.ga_data;
xp->xp_files_menu = (char **)ga_menu.ga_data;
xp->xp_files_info = (char **)ga_info.ga_data;
} else {
// No extra info collected; free the placeholder NULL entries.
for (int i = 0; i < ga_abbr.ga_len; i++) {
xfree(((char **)ga_abbr.ga_data)[i]);
}
xfree(ga_abbr.ga_data);
for (int i = 0; i < ga_kind.ga_len; i++) {
xfree(((char **)ga_kind.ga_data)[i]);
}
xfree(ga_kind.ga_data);
for (int i = 0; i < ga_menu.ga_len; i++) {
xfree(((char **)ga_menu.ga_data)[i]);
}
xfree(ga_menu.ga_data);
for (int i = 0; i < ga_info.ga_len; i++) {
xfree(((char **)ga_info.ga_data)[i]);
}
xfree(ga_info.ga_data);
}
}
/// Expand names with a list returned by a function defined by the user.
@@ -3566,7 +3652,7 @@ static int ExpandUserList(expand_T *xp, char ***matches, int *numMatches)
return FAIL;
}
process_user_list(retlist, matches, numMatches);
process_user_list(retlist, matches, numMatches, xp);
return OK;
}
@@ -3581,7 +3667,7 @@ static int ExpandUserLua(expand_T *xp, int *numMatches, char ***matches)
list_T *const retlist = rettv.vval.v_list;
process_user_list(retlist, matches, numMatches);
process_user_list(retlist, matches, numMatches, xp);
return OK;
}

View File

@@ -35,6 +35,17 @@ typedef struct {
int xp_selected; ///< selected index in completion
char *xp_orig; ///< originally expanded string
char **xp_files; ///< list of files
char **xp_files_abbr; ///< optional parallel array of display
///< strings (override xp_files for the
///< pum text); NULL if unused
char **xp_files_kind; ///< optional parallel array of "kind"
///< strings; NULL if unused
char **xp_files_menu; ///< optional parallel array of "menu"
///< strings (shown after the match);
///< NULL if unused
char **xp_files_info; ///< optional parallel array of "info"
///< strings (shown in info popup);
///< NULL if unused
char *xp_line; ///< text being completed
char xp_buf[EXPAND_BUF_LEN]; ///< buffer for returned match
Direction xp_search_dir; ///< Direction of search

View File

@@ -1251,13 +1251,17 @@ static bool pum_set_selected(int n, int repeat)
update_topline(curwin);
}
const bool save_pum_is_drawn = pum_is_drawn;
// Update the screen before drawing the popup menu.
// Enable updating the status lines.
// TODO(bfredl): can simplify, get rid of the flag munging?
// or at least eliminate extra redraw before win_enter()?
pum_is_visible = false;
pum_is_drawn = false;
update_screen();
pum_is_visible = true;
pum_is_drawn = save_pum_is_drawn;
if (!resized && win_valid(curwin_save)) {
no_u_sync++;
@@ -1268,8 +1272,10 @@ static bool pum_set_selected(int n, int repeat)
// May need to update the screen again when there are
// autocommands involved.
pum_is_visible = false;
pum_is_drawn = false;
update_screen();
pum_is_visible = true;
pum_is_drawn = save_pum_is_drawn;
}
}
}

View File

@@ -4577,6 +4577,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")