-*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
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
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;
}
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.
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);
{
if (xp->xp_numfiles >= 0)
{
+ free_xp_files_extra(xp, xp->xp_numfiles);
FreeWild(xp->xp_numfiles, xp->xp_files);
xp->xp_numfiles = -1;
}
}
if (xp->xp_numfiles == -1)
+ {
FreeWild(numMatches, matches);
+ free_xp_files_extra(xp, numMatches);
+ }
return EXPAND_OK;
}
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;
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
-
- p = vim_strsave(li->li_tv.vval.v_string);
- if (p == NULL)
- break;
-
- if (ga_grow(&ga, 1) == FAIL)
+ 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);
+
+ 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 (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
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")