]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0433: customlist completion cannot supply pum metadata master v9.2.0433
authorYasuhiro Matsumoto <mattn.jp@gmail.com>
Sat, 2 May 2026 16:04:38 +0000 (16:04 +0000)
committerChristian Brabandt <cb@256bit.org>
Sat, 2 May 2026 16:11:01 +0000 (16:11 +0000)
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>
runtime/doc/map.txt
runtime/doc/version9.txt
src/cmdexpand.c
src/structs.h
src/testdir/test_cmdline.vim
src/version.c

index 118e2d44e90a72bf4715a800083a50bd02e54fae..a79c0388cb68a88247a5db500f0060781de0cc65 100644 (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
index c9ede397b868dde9c51e40f44235c16a2babb286..114bdf7dacd2d2eb8b9b9e89031b10a40d4241af 100644 (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 ~
 -----------------
index a4891871f612dc48cbc55f94f1a1a640552a50b5..c265ecbe7f3b5165a876cc8209ce9c2dfcf4dbd0 100644 (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
-
-       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
index 8429ebe29e3468ccc7beccf7e08712d072be6e44..4d92ca75a1893f02d50e8cf434bb231d9abfbd62 100644 (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
index e793292ff7bb67b861b5cd3cbc70bea50892af24..efe73a1e6f4bdef8f05b8c3b1a7a917ac7322e01 100644 (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")
index c75dd6a65f0976b75cc8d646cfeca35a42730c79..3a27d0c52b510f311248490f48aa741bd89d3d62 100644 (file)
@@ -729,6 +729,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    433,
 /**/
     432,
 /**/