diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index b5a0e0fd10..3a7dac6b13 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -5165,12 +5165,13 @@ vim.ui.progress_status() *vim.ui.progress_status()* vim.ui.select({items}, {opts}, {on_choice}) *vim.ui.select()* Prompts the user to pick from a list of items, allowing arbitrary - (potentially asynchronous) work until `on_choice`. + (potentially asynchronous) work until `on_choice`. This is the standard + "picker" interface, used by |z=|, |:tselect|, etc. - Plugins may override `vim.ui.select` to provide a custom "picker" - interface; they are expected to call the `format_item` and `preview_item` - handlers (if any) provided by the caller. They may also use the `kind` - hint (if provided by the caller) to decide how to handle some items. + Plugins may override `vim.ui.select` to provide a custom picker; they are + expected to call the `format_item` and `preview_item` handlers (if any) + provided by the caller. They may also use the `kind` hint (if provided by + the caller) to decide how to handle some items. Note: the default `vim.ui.select` currently doesn't support preview. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 57af11ac71..c24d69405e 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -216,7 +216,10 @@ TUI UI -• todo +• |:tselect| delegates to |vim.ui.select()| instead of a bespoke internal + selection routine. +• |z=| (spell suggest) delegates to |vim.ui.select()| instead of a bespoke + internal selection routine. VIMSCRIPT diff --git a/runtime/lua/vim/_core/spell.lua b/runtime/lua/vim/_core/spell.lua new file mode 100644 index 0000000000..d6bdc010a3 --- /dev/null +++ b/runtime/lua/vim/_core/spell.lua @@ -0,0 +1,36 @@ +local select_blocking = require('vim._core.ui').select_blocking +local N_ = vim.fn.gettext + +local M = {} + +--- @class vim._core.spell.Suggestion +--- @field word string The suggested replacement. +--- @field extra? string Text replaced when wider than the bad span. +--- @field score integer Primary score. +--- @field altscore? integer Secondary score (only set when 'spellsuggest' contains "double" or "best"). +--- @field salscore? boolean True if the score came from sound-alike comparison (only set alongside `altscore`). + +--- Called from `spell_suggest()` (`z=`) to let the user pick from `items` via +--- |vim.ui.select()|. +--- +--- @param items vim._core.spell.Suggestion[] +--- @param bad string The misspelled word being replaced. +--- @return integer? # 1-based index of the chosen suggestion, or nil if cancelled. +function M.suggest_select(items, bad) + return select_blocking(items, { + prompt = N_('Change "%s" to:'):format(bad), + kind = 'spell', + format_item = function(s) + local extra = s.extra and (' < "' .. s.extra .. '"') or '' + local score = '' + if vim.o.verbose > 0 then + score = s.altscore + and (' (%s%d - %d)'):format(s.salscore and 's ' or '', s.score, s.altscore) + or (' (%d)'):format(s.score) + end + return ('"%s"%s%s'):format(s.word, extra, score) + end, + }) +end + +return M diff --git a/runtime/lua/vim/_core/tag.lua b/runtime/lua/vim/_core/tag.lua new file mode 100644 index 0000000000..9d6c3cffbd --- /dev/null +++ b/runtime/lua/vim/_core/tag.lua @@ -0,0 +1,44 @@ +local select_blocking = require('vim._core.ui').select_blocking +local N_ = vim.fn.gettext + +local M = {} + +--- @class vim._core.tag.Match +--- @field tag string +--- @field kind? string +--- @field pri string Priority code, e.g. "FSC" — see `:h tag-priority`. +--- @field file string +--- @field extra? string +--- @field cur boolean True if this is the currently-active tagstack match. + +--- Called from `do_tag()` (`:tselect`, ambiguous `:tag`, etc.) to let the user +--- pick from `matches` via |vim.ui.select()|. +--- +--- @param items vim._core.tag.Match[] One per matching tag. +--- @return integer? # 1-based index of the chosen tag, or nil if cancelled. +function M.select(items) + local taglen = 18 + for _, m in ipairs(items) do + taglen = math.max(taglen, vim.fn.strdisplaywidth(m.tag) + 2) + end + + return select_blocking(items, { + prompt = N_('Type number and (q or empty cancels):'), + kind = 'tag', + format_item = function(m) + local marker = m.cur and '>' or ' ' + local kind = m.kind or '' + local extra = m.extra and (' ' .. m.extra) or '' + return ('%s %s %-4s %-' .. taglen .. 's %s%s'):format( + marker, + m.pri, + kind, + m.tag, + m.file, + extra + ) + end, + }) +end + +return M diff --git a/runtime/lua/vim/_core/ui.lua b/runtime/lua/vim/_core/ui.lua new file mode 100644 index 0000000000..4bb1dc0c7b --- /dev/null +++ b/runtime/lua/vim/_core/ui.lua @@ -0,0 +1,23 @@ +local M = {} + +--- Wait for |vim.ui.select()| and return the selected index. The default vim.ui.select impl +--- (inputlist()) is synchronous, but this also handles async pickers (fzf-lua, telescope, …). +--- +--- @param items table Items to choose from. +--- @param opts table Forwarded to |vim.ui.select()|. +--- @return integer? # 1-based index of the chosen item, or nil if cancelled/interrupted. +function M.select_blocking(items, opts) + local choice ---@type integer? + local done = false + vim.ui.select(items, opts or {}, function(_, idx) + choice = idx + done = true + end) + -- vim.wait returns false on timeout (math.huge means never) or interrupt (-2). + vim.wait(math.huge, function() + return done + end) + return choice +end + +return M diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 871a159346..665fd19e9e 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -26,11 +26,11 @@ local M = vim._defer_require('vim.ui', { ---@field kind? string --- Prompts the user to pick from a list of items, allowing arbitrary (potentially asynchronous) ---- work until `on_choice`. +--- work until `on_choice`. This is the standard "picker" interface, used by |z=|, |:tselect|, etc. --- ---- Plugins may override `vim.ui.select` to provide a custom "picker" interface; they are expected ---- to call the `format_item` and `preview_item` handlers (if any) provided by the caller. They may ---- also use the `kind` hint (if provided by the caller) to decide how to handle some items. +--- Plugins may override `vim.ui.select` to provide a custom picker; they are expected to call the +--- `format_item` and `preview_item` handlers (if any) provided by the caller. They may also use the +--- `kind` hint (if provided by the caller) to decide how to handle some items. --- --- Note: the default `vim.ui.select` currently doesn't support preview. --- diff --git a/src/nvim/spellsuggest.c b/src/nvim/spellsuggest.c index 7a24653290..4f1bae9f9b 100644 --- a/src/nvim/spellsuggest.c +++ b/src/nvim/spellsuggest.c @@ -28,6 +28,7 @@ #include "nvim/hashtab_defs.h" #include "nvim/highlight_defs.h" #include "nvim/input.h" +#include "nvim/lua/executor.h" #include "nvim/macros_defs.h" #include "nvim/mbyte.h" #include "nvim/mbyte_defs.h" @@ -432,6 +433,68 @@ int spell_check_sps(void) return OK; } +/// Let the user pick a spell suggestion. Delegates to `vim.ui.select()`. +/// +/// @return 1-based index of the chosen suggestion, or 0 if cancelled. +static int select_spell_suggestion(suginfo_T *sug) +{ + typval_T items_tv; + tv_list_alloc_ret(&items_tv, sug->su_ga.ga_len); + + for (int i = 0; i < sug->su_ga.ga_len; i++) { + suggest_T *stp = &SUG(sug->su_ga, i); + + dict_T *d = tv_dict_alloc(); + + // The suggested word may replace only part of the bad word; append the + // unreplaced tail to form the user-visible "word". + int el = sug->su_badlen - stp->st_orglen; + if (el > 0) { + char *word = xmallocz((size_t)stp->st_wordlen + (size_t)el); + memcpy(word, stp->st_word, (size_t)stp->st_wordlen); + memcpy(word + stp->st_wordlen, sug->su_badptr + stp->st_orglen, (size_t)el); + tv_dict_add_allocated_str(d, S_LEN("word"), word); + } else { + tv_dict_add_str(d, S_LEN("word"), stp->st_word); + } + + // The suggestion may replace MORE than su_badlen of the bad text; + // capture that wider span as `extra`. + if (sug->su_badlen < stp->st_orglen) { + char *extra = xstrnsave(sug->su_badptr, (size_t)stp->st_orglen); + tv_dict_add_allocated_str(d, S_LEN("extra"), extra); + } + + // Pass raw scoring data; Lua decides whether/how to render. + // `altscore`/`salscore` are only meaningful with SPS_DOUBLE|SPS_BEST. + tv_dict_add_nr(d, S_LEN("score"), stp->st_score); + if (sps_flags & (SPS_DOUBLE | SPS_BEST)) { + tv_dict_add_nr(d, S_LEN("altscore"), stp->st_altscore); + tv_dict_add_bool(d, S_LEN("salscore"), + stp->st_salscore ? kBoolVarTrue : kBoolVarFalse); + } + + typval_T item = { .v_type = VAR_DICT, .vval.v_dict = d }; + tv_list_append_tv(items_tv.vval.v_list, &item); + } + + typval_T bad_tv = { .v_type = VAR_STRING, + .vval.v_string = xstrnsave(sug->su_badptr, (size_t)sug->su_badlen) }; + typval_T lua_args[] = { items_tv, bad_tv, { .v_type = VAR_UNKNOWN } }; + typval_T rettv = TV_INITIAL_VALUE; + nlua_call_vimfn("vim._core.spell", "suggest_select", lua_args, &rettv); + + int idx = 0; + if (rettv.v_type == VAR_NUMBER) { + idx = (int)rettv.vval.v_number; + } + + tv_clear(&items_tv); + tv_clear(&bad_tv); + tv_clear(&rettv); + return idx; +} + /// "z=": Find badly spelled word under or after the cursor. /// Give suggestions for the properly spelled word. /// In Visual mode use the highlighted word as the bad word. @@ -439,7 +502,6 @@ int spell_check_sps(void) void spell_suggest(int count) { pos_T prev_cursor = curwin->w_cursor; - bool mouse_used = false; int badlen = 0; int msg_scroll_save = msg_scroll; const int wo_spell_save = curwin->w_p_spell; @@ -512,91 +574,17 @@ void spell_suggest(int count) true, need_cap, true); int selected = count; - msg_ext_set_kind("confirm"); if (GA_EMPTY(&sug.su_ga)) { + msg_ext_set_kind("wmsg"); msg(_("No suggestions"), 0); } else if (count > 0) { if (count > sug.su_ga.ga_len) { - smsg(0, _("Only %" PRId64 " suggestions"), - (int64_t)sug.su_ga.ga_len); + msg_ext_set_kind("wmsg"); + smsg(0, _("Only %" PRId64 " suggestions"), (int64_t)sug.su_ga.ga_len); } } else { - // When 'rightleft' is set the list is drawn right-left. - cmdmsg_rl = curwin->w_p_rl; - - // List the suggestions. - msg_start(); - msg_row = Rows - 1; // for when 'cmdheight' > 1 - lines_left = Rows; // avoid more prompt - char *fmt = _("Change \"%.*s\" to:"); - if (cmdmsg_rl && strncmp(fmt, "Change", 6) == 0) { - // And now the rabbit from the high hat: Avoid showing the - // untranslated message rightleft. - fmt = ":ot \"%.*s\" egnahC"; - } - vim_snprintf(IObuff, IOSIZE, fmt, sug.su_badlen, sug.su_badptr); - msg_puts(IObuff); - msg_clr_eos(); - msg_putchar('\n'); - - msg_scroll = true; - for (int i = 0; i < sug.su_ga.ga_len; i++) { - suggest_T *stp = &SUG(sug.su_ga, i); - - // The suggested word may replace only part of the bad word, add - // the not replaced part. But only when it's not getting too long. - char wcopy[MAXWLEN + 2]; - xstrlcpy(wcopy, stp->st_word, MAXWLEN + 1); - int el = sug.su_badlen - stp->st_orglen; - if (el > 0 && stp->st_wordlen + el <= MAXWLEN) { - assert(sug.su_badptr != NULL); - xmemcpyz(wcopy + stp->st_wordlen, sug.su_badptr + stp->st_orglen, (size_t)el); - } - vim_snprintf(IObuff, IOSIZE, "%2d", i + 1); - if (cmdmsg_rl) { - rl_mirror_ascii(IObuff, NULL); - } - msg_puts(IObuff); - - vim_snprintf(IObuff, IOSIZE, " \"%s\"", wcopy); - msg_puts(IObuff); - - // The word may replace more than "su_badlen". - if (sug.su_badlen < stp->st_orglen) { - vim_snprintf(IObuff, IOSIZE, _(" < \"%.*s\""), - stp->st_orglen, sug.su_badptr); - msg_puts(IObuff); - } - - if (p_verbose > 0) { - // Add the score. - if (sps_flags & (SPS_DOUBLE | SPS_BEST)) { - vim_snprintf(IObuff, IOSIZE, " (%s%d - %d)", - stp->st_salscore ? "s " : "", - stp->st_score, stp->st_altscore); - } else { - vim_snprintf(IObuff, IOSIZE, " (%d)", - stp->st_score); - } - if (cmdmsg_rl) { - // Mirror the numbers, but keep the leading space. - rl_mirror_ascii(IObuff + 1, NULL); - } - msg_advance(30); - msg_puts(IObuff); - } - if (!ui_has(kUIMessages) || i < sug.su_ga.ga_len - 1) { - msg_putchar('\n'); - } - } - - cmdmsg_rl = false; - msg_col = 0; - // Ask for choice. - selected = prompt_for_input(NULL, 0, false, &mouse_used); - if (mouse_used) { - selected = sug.su_ga.ga_len + 1 - (cmdline_row - mouse_row); - } + // Ask the user (via vim.ui.select) to pick a suggestion. + selected = select_spell_suggestion(&sug); lines_left = Rows; // avoid more prompt // don't delay for 'smd' in normal_cmd() diff --git a/src/nvim/tag.c b/src/nvim/tag.c index fb2ae19d79..aec12afd8b 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -38,6 +38,7 @@ #include "nvim/highlight_defs.h" #include "nvim/input.h" #include "nvim/insexpand.h" +#include "nvim/lua/executor.h" #include "nvim/macros_defs.h" #include "nvim/mark.h" #include "nvim/mark_defs.h" @@ -651,26 +652,13 @@ void do_tag(char *tag, int type, int count, int forceit, bool verbose) } g_do_tagpreview = 0; } else { - bool ask_for_selection = false; - if (type == DT_TAG && *tag != NUL) { // If a count is supplied to the ":tag " command, then // jump to count'th matching tag. cur_match = count > 0 ? count - 1 : 0; } else if (type == DT_SELECT || (type == DT_JUMP && num_matches > 1)) { - print_tag_list(new_tag, use_tagstack, num_matches, matches); - ask_for_selection = true; - } else if (type == DT_LTAG) { - if (add_llist_tags(tag, num_matches, matches) == FAIL) { - goto end_do_tag; - } - - cur_match = 0; // Jump to the first tag - } - - if (ask_for_selection) { - // Ask to select a tag from the list. - int i = prompt_for_input(NULL, 0, false, NULL); + // Ask the user (via vim.ui.select) to pick a tag. + int i = select_tag_match(new_tag, use_tagstack, num_matches, matches); if (i <= 0 || i > num_matches || got_int) { // no valid choice: don't change anything if (use_tagstack) { @@ -680,6 +668,12 @@ void do_tag(char *tag, int type, int count, int forceit, bool verbose) break; } cur_match = i - 1; + } else if (type == DT_LTAG) { + if (add_llist_tags(tag, num_matches, matches) == FAIL) { + goto end_do_tag; + } + + cur_match = 0; // Jump to the first tag } if (cur_match >= num_matches) { @@ -797,179 +791,57 @@ end_do_tag: xfree(tofree); } -// List all the matching tags. -static void print_tag_list(bool new_tag, bool use_tagstack, int num_matches, char **matches) +/// Let the user pick from `matches`. Delegates to `vim.ui.select()`. +/// +/// @return 1-based index of the chosen tag, or 0 if cancelled. +static int select_tag_match(bool new_tag, bool use_tagstack, int num_matches, char **matches) { taggy_T *tagstack = curwin->w_tagstack; int tagstackidx = curwin->w_tagstackidx; - tagptrs_T tagp; - // Assume that the first match indicates how long the tags can - // be, and align the file names to that. - parse_match(matches[0], &tagp); - int taglen = MAX((int)(tagp.tagname_end - tagp.tagname + 2), 18); - if (taglen > Columns - 25) { - taglen = MAXCOL; - } - if (msg_col == 0) { - msg_didout = false; // overwrite previous message - } - msg_ext_set_kind("confirm"); - msg_start(); - msg_puts_hl(_(" # pri kind tag"), HLF_T, false); - msg_clr_eos(); - taglen_advance(taglen); - msg_puts_hl(_("file\n"), HLF_T, false); + typval_T items_tv; + tv_list_alloc_ret(&items_tv, num_matches); - for (int i = 0; i < num_matches && !got_int; i++) { - parse_match(matches[i], &tagp); - if (!new_tag && ( - (g_do_tagpreview != 0 - && i == ptag_entry.cur_match) - || (use_tagstack - && i == tagstack[tagstackidx].cur_match))) { - *IObuff = '>'; - } else { - *IObuff = ' '; - } - vim_snprintf(IObuff + 1, IOSIZE - 1, - "%2d %s ", i + 1, - mt_names[matches[i][0] & MT_MASK]); - msg_puts(IObuff); - if (tagp.tagkind != NULL) { - msg_outtrans_len(tagp.tagkind, (int)(tagp.tagkind_end - tagp.tagkind), 0, false); - } - msg_advance(13); - msg_outtrans_len(tagp.tagname, (int)(tagp.tagname_end - tagp.tagname), HLF_T, false); - msg_putchar(' '); - taglen_advance(taglen); - - // Find out the actual file name. If it is long, truncate - // it and put "..." in the middle - const char *p = tag_full_fname(&tagp); - if (p != NULL) { - msg_outtrans(p, HLF_D, false); - XFREE_CLEAR(p); - } - if (msg_col > 0) { - msg_putchar('\n'); - } - if (got_int) { - break; - } - msg_advance(15); - - // print any extra fields - const char *command_end = tagp.command_end; - if (command_end != NULL) { - p = command_end + 3; - while (*p && *p != '\r' && *p != '\n') { - while (*p == TAB) { - p++; - } - - // skip "file:" without a value (static tag) - if (strncmp(p, "file:", 5) == 0 && ascii_isspace(p[5])) { - p += 5; - continue; - } - // skip "kind:" and "" - if (p == tagp.tagkind - || (p + 5 == tagp.tagkind - && strncmp(p, "kind:", 5) == 0)) { - p = tagp.tagkind_end; - continue; - } - // print all other extra fields - int hl_id = HLF_CM; - while (*p && *p != '\r' && *p != '\n') { - if (msg_col + ptr2cells(p) >= Columns) { - msg_putchar('\n'); - if (got_int) { - break; - } - msg_advance(15); - } - p = msg_outtrans_one(p, hl_id, false); - if (*p == TAB) { - msg_puts_hl(" ", hl_id, false); - break; - } - if (*p == ':') { - hl_id = 0; - } - } - } - if (msg_col > 15) { - msg_putchar('\n'); - if (got_int) { - break; - } - msg_advance(15); - } - } else { - for (p = tagp.command; - *p && *p != '\r' && *p != '\n'; - p++) {} - command_end = p; - } - - // Put the info (in several lines) at column 15. - // Don't display "/^" and "?^". - p = tagp.command; - if (*p == '/' || *p == '?') { - p++; - if (*p == '^') { - p++; - } - } - // Remove leading whitespace from pattern - while (p != command_end && ascii_isspace(*p)) { - p++; - } - - while (p != command_end) { - if (msg_col + (*p == TAB ? 1 : ptr2cells(p)) > Columns) { - msg_putchar('\n'); - } - if (got_int) { - break; - } - msg_advance(15); - - // skip backslash used for escaping a command char or - // a backslash - if (*p == '\\' && (*(p + 1) == *tagp.command - || *(p + 1) == '\\')) { - p++; - } - - if (*p == TAB) { - msg_putchar(' '); - p++; - } else { - p = msg_outtrans_one(p, 0, false); - } - - // don't display the "$/;\"" and "$?;\"" - if (p == command_end - 2 && *p == '$' - && *(p + 1) == *tagp.command) { - break; - } - // don't display matching '/' or '?' - if (p == command_end - 1 && *p == *tagp.command - && (*p == '/' || *p == '?')) { - break; - } - } - if (msg_col && (!ui_has(kUIMessages) || i < num_matches - 1)) { - msg_putchar('\n'); - } + for (int i = 0; i < num_matches; i++) { os_breakcheck(); + if (got_int) { + tv_clear(&items_tv); + return 0; + } + tagptrs_T tagp; + parse_match(matches[i], &tagp); + + bool cur = !new_tag + && ((g_do_tagpreview != 0 && i == ptag_entry.cur_match) + || (use_tagstack && i == tagstack[tagstackidx].cur_match)); + + dict_T *d = tv_dict_alloc(); + tv_dict_add_str_len(d, S_LEN("tag"), tagp.tagname, (int)(tagp.tagname_end - tagp.tagname)); + tv_dict_add_str(d, S_LEN("pri"), mt_names[matches[i][0] & MT_MASK]); + if (tagp.tagkind != NULL) { + tv_dict_add_str_len(d, S_LEN("kind"), tagp.tagkind, (int)(tagp.tagkind_end - tagp.tagkind)); + } + char *fname = tag_full_fname(&tagp); + tv_dict_add_str(d, S_LEN("file"), fname != NULL ? fname : ""); + xfree(fname); + tv_dict_add_bool(d, S_LEN("cur"), cur ? kBoolVarTrue : kBoolVarFalse); + + typval_T item = { .v_type = VAR_DICT, .vval.v_dict = d }; + tv_list_append_tv(items_tv.vval.v_list, &item); } - if (got_int) { - got_int = false; // only stop the listing + + typval_T lua_args[] = { items_tv, { .v_type = VAR_UNKNOWN } }; + typval_T rettv = TV_INITIAL_VALUE; + nlua_call_vimfn("vim._core.tag", "select", lua_args, &rettv); + + int idx = 0; + if (rettv.v_type == VAR_NUMBER) { + idx = (int)rettv.vval.v_number; } + + tv_clear(&items_tv); + tv_clear(&rettv); + return idx; } /// Add the matching tags to the location list for the current diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index 474519b16e..313900938f 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -4,7 +4,6 @@ local Screen = require('test.functional.ui.screen') local uv = vim.uv local eq = t.eq -local pcall_err = t.pcall_err local matches = t.matches local feed = n.feed local eval = n.eval @@ -231,10 +230,13 @@ describe('vim._core', function() 'vim._core.options', 'vim._core.server', 'vim._core.shared', + 'vim._core.spell', 'vim._core.stringbuffer', 'vim._core.system', 'vim._core.table', + 'vim._core.tag', 'vim._core.time', + 'vim._core.ui', 'vim._core.ui2', 'vim._core.util', 'vim._core.vimfn', diff --git a/test/functional/lua/ui_select_spec.lua b/test/functional/lua/ui_select_spec.lua new file mode 100644 index 0000000000..e073bf95c4 --- /dev/null +++ b/test/functional/lua/ui_select_spec.lua @@ -0,0 +1,168 @@ +-- Tests for vim.ui.select(), including integration with builtins (:tselect, z=). + +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local clear = n.clear +local exec_lua = n.exec_lua +local api = n.api +local eq = t.eq +local write_file = t.write_file + +before_each(clear) + +describe('vim.ui.select()', function() + it('can select an item', function() + local result = exec_lua [[ + local items = { + { name = 'Item 1' }, + { name = 'Item 2' }, + } + local opts = { + format_item = function(entry) + return entry.name + end + } + local selected + local cb = function(item) + selected = item + end + -- inputlist would require input and block the test; + local choices + vim.fn.inputlist = function(x) + choices = x + return 1 + end + vim.ui.select(items, opts, cb) + vim.wait(100, function() return selected ~= nil end) + return {selected, choices} + ]] + eq({ name = 'Item 1' }, result[1]) + eq({ + 'Select one of:', + '1: Item 1', + '2: Item 2', + }, result[2]) + end) + + describe('via :tselect', function() + it('passes items and applies the chosen index', function() + -- Create dummy source files so the jump succeeds. + write_file('XselTagA.c', 'int foo;\n') + write_file('XselTagB.c', 'int foo = 1;\n') + finally(function() + os.remove('XselTagA.c') + os.remove('XselTagB.c') + os.remove('XselTags') + end) + write_file( + 'XselTags', + '!_TAG_FILE_FORMAT\t2\t/extended format/\n' + .. 'foo\tXselTagA.c\t/^int foo;$/;"\tv\n' + .. 'foo\tXselTagB.c\t/^int foo = 1;$/;"\tv\n' + ) + + local got = exec_lua(function() + vim.opt.tags = 'XselTags' + local captured ---@type table? + vim.ui.select = function(items, opts, on_choice) + captured = { items = items, kind = opts.kind } + -- Pick the second match. + on_choice(items[2], 2) + end + vim.cmd('tselect foo') + return { + kind = captured and captured.kind, + nitems = captured and #captured.items, + item1_tag = captured and captured.items[1].tag, + item2_file = captured and captured.items[2].file, + bufname = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':t'), + } + end) + + eq('tag', got.kind) + eq(2, got.nitems) + eq('foo', got.item1_tag) + eq('XselTagB.c', got.item2_file) + -- Picking item 2 should land us in XselTagB.c. + eq('XselTagB.c', got.bufname) + end) + + it('keeps the buffer unchanged when the user cancels', function() + write_file('XselTagA.c', 'int foo;\n') + write_file('XselTagB.c', 'int foo = 1;\n') + finally(function() + os.remove('XselTagA.c') + os.remove('XselTagB.c') + os.remove('XselTags') + end) + write_file( + 'XselTags', + '!_TAG_FILE_FORMAT\t2\t/extended format/\n' + .. 'foo\tXselTagA.c\t/^int foo;$/;"\tv\n' + .. 'foo\tXselTagB.c\t/^int foo = 1;$/;"\tv\n' + ) + + api.nvim_set_option_value('tags', 'XselTags', {}) + + local before = api.nvim_buf_get_name(0) + exec_lua(function() + vim.ui.select = function(_, _, on_choice) + on_choice(nil, nil) + end + vim.cmd('tselect foo') + end) + + eq(before, api.nvim_buf_get_name(0)) + end) + end) + + describe('via z=', function() + it('passes items and applies the chosen suggestion', function() + api.nvim_set_option_value('spell', true, {}) + api.nvim_set_option_value('spelllang', 'en_us', {}) + + api.nvim_buf_set_lines(0, 0, -1, false, { 'helo' }) + + local got = exec_lua(function() + vim.cmd('normal! gg0') + local captured ---@type table? + vim.ui.select = function(items, opts, on_choice) + captured = { items = items, kind = opts.kind, prompt = opts.prompt } + -- Pick the first suggestion. + on_choice(items[1], 1) + end + vim.cmd('normal! z=') + return { + kind = captured and captured.kind, + prompt = captured and captured.prompt, + item1_word = captured and captured.items[1].word, + line = vim.api.nvim_buf_get_lines(0, 0, -1, false)[1], + } + end) + + eq('spell', got.kind) + -- prompt should contain the misspelled word + t.matches('helo', got.prompt) + -- The first suggestion replaced the bad word. + t.neq('helo', got.line) + eq(got.item1_word, got.line) + end) + + it('keeps the word unchanged when the user cancels', function() + api.nvim_set_option_value('spell', true, {}) + api.nvim_set_option_value('spelllang', 'en_us', {}) + + api.nvim_buf_set_lines(0, 0, -1, false, { 'helo' }) + + exec_lua(function() + vim.cmd('normal! gg0') + vim.ui.select = function(_, _, on_choice) + on_choice(nil, nil) + end + vim.cmd('normal! z=') + end) + + eq('helo', api.nvim_buf_get_lines(0, 0, -1, false)[1]) + end) + end) +end) diff --git a/test/functional/lua/ui_spec.lua b/test/functional/lua/ui_spec.lua index d08dc86490..5864dca484 100644 --- a/test/functional/lua/ui_spec.lua +++ b/test/functional/lua/ui_spec.lua @@ -2,7 +2,6 @@ local t = require('test.testutil') local n = require('test.functional.testnvim')() local eq = t.eq -local pcall_err = t.pcall_err local ok = t.ok local exec_lua = n.exec_lua local clear = n.clear @@ -17,41 +16,6 @@ describe('vim.ui', function() clear({ args_rm = { '-u' }, args = { '--clean' } }) end) - describe('select()', function() - it('can select an item', function() - local result = exec_lua [[ - local items = { - { name = 'Item 1' }, - { name = 'Item 2' }, - } - local opts = { - format_item = function(entry) - return entry.name - end - } - local selected - local cb = function(item) - selected = item - end - -- inputlist would require input and block the test; - local choices - vim.fn.inputlist = function(x) - choices = x - return 1 - end - vim.ui.select(items, opts, cb) - vim.wait(100, function() return selected ~= nil end) - return {selected, choices} - ]] - eq({ name = 'Item 1' }, result[1]) - eq({ - 'Select one of:', - '1: Item 1', - '2: Item 2', - }, result[2]) - end) - end) - describe('input()', function() it('can input text', function() local result = exec_lua [[ diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua index 861882c296..44af9d478f 100644 --- a/test/functional/ui/messages_spec.lua +++ b/test/functional/ui/messages_spec.lua @@ -400,7 +400,8 @@ describe('ui/ext_messages', function() { content = { { '' } }, pos = 0, - prompt = 'Type number and (q or empty cancels): ', + -- Default vim.ui.select uses this prompt. + prompt = 'Type number and or click with the mouse (q or empty cancels): ', }, }, -- Message depends on runtimepath, only test the static text... @@ -408,13 +409,12 @@ describe('ui/ext_messages', function() for _, msg in ipairs(screen.messages) do eq(false, msg.history) eq('confirm', msg.kind) - eq(' # pri kind tag', msg.content[1][2]) - eq('\n ', msg.content[2][2]) - eq('file\n', msg.content[3][2]) - eq('> 1 F ', msg.content[4][2]) - eq('help.txt', msg.content[5][2]) - eq(' \n ', msg.content[6][2]) - eq('\n *help.txt*', msg.content[#msg.content][2]) + local text = '' -- Concatenate all chunks. + for _, chunk in ipairs(msg.content) do + text = text .. (#chunk >= 2 and chunk[2] or chunk[1]) + end + t.matches('^Type number and %(q or empty cancels%):\n', text) + t.matches('1: > F%s+help%.txt%s+', text) end screen.messages = {} end, @@ -1286,7 +1286,7 @@ stack traceback: }, messages = { { - content = { { 'Change "helllo" to:\n 1 "Hello"\n 2 "Hallo"\n 3 "Hullo"' } }, + content = { { 'Change "helllo" to:\n1: "Hello"\n2: "Hallo"\n3: "Hullo"' } }, kind = 'confirm', }, }, diff --git a/test/old/testdir/test_spell.vim b/test/old/testdir/test_spell.vim index 44a548f400..0d0ca3de57 100644 --- a/test/old/testdir/test_spell.vim +++ b/test/old/testdir/test_spell.vim @@ -466,13 +466,12 @@ func Test_spellsuggest_option_number() call assert_equal('A baord', getline(1)) let a = execute('norm $z=') + " Nvim: z= goes through vim.ui.select(). call assert_equal( \ "\n" \ .. "Change \"baord\" to:\n" - \ .. " 1 \"board\"\n" - \ .. " 2 \"bard\"\n" - "\ Nvim: Prompt message is sent to cmdline prompt. - "\ .. "Type number and or click with the mouse (q or empty cancels): ", a) + \ .. "1: \"board\"\n" + \ .. "2: \"bard\"\n" \ , a) set spell spellsuggest=0 @@ -505,14 +504,13 @@ func Test_spellsuggest_option_expr() new call setline(1, 'baord') let a = execute('norm z=') + " Nvim: z= goes through vim.ui.select(). call assert_equal( \ "\n" \ .. "Change \"baord\" to:\n" - \ .. " 1 \"BARD\"\n" - \ .. " 2 \"BOARD\"\n" - \ .. " 3 \"BROAD\"\n" - "\ Nvim: Prompt message is sent to cmdline prompt. - "\ .. "Type number and or click with the mouse (q or empty cancels): ", a) + \ .. "1: \"BARD\"\n" + \ .. "2: \"BOARD\"\n" + \ .. "3: \"BROAD\"\n" \ , a) " With verbose, z= should show the score i.e. word length with @@ -522,11 +520,9 @@ func Test_spellsuggest_option_expr() call assert_equal( \ "\n" \ .. "Change \"baord\" to:\n" - \ .. " 1 \"BARD\" (4 - 0)\n" - \ .. " 2 \"BOARD\" (5 - 0)\n" - \ .. " 3 \"BROAD\" (5 - 0)\n" - "\ Nvim: Prompt message is sent to cmdline prompt. - "\ .. "Type number and or click with the mouse (q or empty cancels): ", a) + \ .. "1: \"BARD\" (4 - 0)\n" + \ .. "2: \"BOARD\" (5 - 0)\n" + \ .. "3: \"BROAD\" (5 - 0)\n" \ , a) set spell& spellsuggest& verbose& diff --git a/test/old/testdir/test_tagjump.vim b/test/old/testdir/test_tagjump.vim index 01b4b8e75b..9a791da3d8 100644 --- a/test/old/testdir/test_tagjump.vim +++ b/test/old/testdir/test_tagjump.vim @@ -1239,17 +1239,12 @@ func Test_tselect_listing() call feedkeys("\", "t") let l = split(execute("tselect first"), "\n") + " Nvim: :tselect goes through vim.ui.select(). let expected =<< [DATA] - # pri kind tag file - 1 FS v first Xfoo - typeref:typename:int - 1 - 2 FS v first Xfoo - typeref:typename:char - 2 +Type number and (q or empty cancels): +1: FS v first Xfoo +2: FS v first Xfoo [DATA] -" Type number and (q or empty cancels): -" Nvim: Prompt message is sent to cmdline prompt. call assert_equal(expected, l)