feat(ui): use vim.ui.select for :tselect, z= #39478

Problem:
`:tselect` and `z=` (spell suggest) have their own bespoke select menus.

Solution:
- Delegate to `vim.ui.select` instead.
- Bonus:
  - `:tselect` gains mouse support. `print_tag_list` didn't suport mouseclick.

This causes some minor regressions, which are not blockers:

- `z=` no longer draws the list right-left if 'rightleft' is set.
  - TODO: can/should `vim.ui.select` / `vim.fn.inputlist()` handle that?
- `:tselect`
  - No "column" headings (`# pri kind tag file`).
  - No highlighting: (HLF_T: tag name, HLF_D: file, HLF_CM: extra fields).
  - TODO: can `vim.ui.select()` support highlighted chunks (`[[text, hl_id], ...]`) ?

fix https://github.com/neovim/neovim/issues/25814
fix https://github.com/neovim/neovim/issues/31987
This commit is contained in:
Justin M. Keyes
2026-04-28 18:29:17 -04:00
committed by GitHub
parent 33ea63011c
commit 55ceb314ca
14 changed files with 431 additions and 339 deletions
+6 -5
View File
@@ -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.
+4 -1
View File
@@ -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
+36
View File
@@ -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
+44
View File
@@ -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 <Enter> (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
+23
View File
@@ -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
+4 -4
View File
@@ -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.
---
+68 -80
View File
@@ -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()
+52 -180
View File
@@ -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 <name>" 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:<kind>" and "<kind>"
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
+3 -1
View File
@@ -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',
+168
View File
@@ -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)
-36
View File
@@ -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 [[
+9 -9
View File
@@ -400,7 +400,8 @@ describe('ui/ext_messages', function()
{
content = { { '' } },
pos = 0,
prompt = 'Type number and <Enter> (q or empty cancels): ',
-- Default vim.ui.select uses this prompt.
prompt = 'Type number and <Enter> 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 <Enter> %(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',
},
},
+10 -14
View File
@@ -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 <Enter> 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 <Enter> 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 <Enter> 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&
+4 -9
View File
@@ -1239,17 +1239,12 @@ func Test_tselect_listing()
call feedkeys("\<CR>", "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 <Enter> (q or empty cancels):
1: FS v first Xfoo
2: FS v first Xfoo
[DATA]
" Type number and <Enter> (q or empty cancels):
" Nvim: Prompt message is sent to cmdline prompt.
call assert_equal(expected, l)