Files
neovim/test/functional/lua/ui_select_spec.lua
T
Justin M. Keyes 55ceb314ca 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
2026-04-28 18:29:17 -04:00

169 lines
4.9 KiB
Lua

-- 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)