From: Yasuhiro Matsumoto Date: Sat, 2 May 2026 16:04:38 +0000 (+0000) Subject: patch 9.2.0433: customlist completion cannot supply pum metadata X-Git-Tag: v9.2.0433^0 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5c700152ae23c91b6edef3fa3e7ba06d40be0f9e;p=thirdparty%2Fvim.git 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 Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt index 118e2d44e9..a79c0388cb 100644 --- a/runtime/doc/map.txt +++ b/runtime/doc/map.txt @@ -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 diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index c9ede397b8..114bdf7dac 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -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 ~ ----------------- diff --git a/src/cmdexpand.c b/src/cmdexpand.c index a4891871f6..c265ecbe7f 100644 --- a/src/cmdexpand.c +++ b/src/cmdexpand.c @@ -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 - - 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 diff --git a/src/structs.h b/src/structs.h index 8429ebe29e..4d92ca75a1 100644 --- a/src/structs.h +++ b/src/structs.h @@ -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 diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim index e793292ff7..efe73a1e6f 100644 --- a/src/testdir/test_cmdline.vim +++ b/src/testdir/test_cmdline.vim @@ -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 + + " 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\\\"\", '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 + call feedkeys(":DictAbbrCmd \\\"\", '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") diff --git a/src/version.c b/src/version.c index c75dd6a65f..3a27d0c52b 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 433, /**/ 432, /**/