diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt index b588ffc05f..47f0672864 100644 --- a/runtime/doc/map.txt +++ b/runtime/doc/map.txt @@ -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 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 6857cd4eae..d22453d9d2 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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 || support `file://…` URIs. • |:log| opens log files. • |ZR| restarts Nvim (|:restart|). diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index acd5d05fd3..3cc2fe7fc1 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -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; } diff --git a/src/nvim/cmdexpand_defs.h b/src/nvim/cmdexpand_defs.h index 532ea7fd11..c78edf50e0 100644 --- a/src/nvim/cmdexpand_defs.h +++ b/src/nvim/cmdexpand_defs.h @@ -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 diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c index bf080136c2..846335738d 100644 --- a/src/nvim/popupmenu.c +++ b/src/nvim/popupmenu.c @@ -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; } } } diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim index e122758159..9637ae5af4 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -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 + + " 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")