feat(ui): add vim.ui.select and use in code actions (#15771)

Continuation of https://github.com/neovim/neovim/pull/15202

A plugin like telescope could override it with a fancy implementation
and then users would get the telescope-ui within each plugin that
utilizes the vim.ui.select function.

There are some plugins which override the `textDocument/codeAction`
handler solely to provide a different UI. With custom client commands and
soon codeAction resolve support, it becomes more difficult to implement
the handler right - so having a dedicated way to override the picking
function will be useful.
This commit is contained in:
Mathias Fußenegger 2021-09-27 21:57:28 +02:00 committed by GitHub
parent 6736ee8be5
commit 63fde086d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 145 additions and 34 deletions

View File

@ -1645,4 +1645,25 @@ uri_to_fname({uri}) *vim.uri_to_fname()*
Return: ~
Filename
==============================================================================
Lua module: ui *lua-ui*
select({items}, {opts}, {on_choice}) *vim.ui.select()*
Prompts the user to pick a single item from a collection of
entries
Parameters: ~
{items} table Arbitrary items
{opts} table Additional options
• prompt (string|nil) Text of the prompt.
Defaults to `Select one of:`
• format_item (function item -> text)
Function to format an individual item from
`items` . Defaults to `tostring` .
{on_choice} function ((item|nil, idx|nil) -> ()) Called
once the user made a choice. `idx` is the
1-based index of `item` within `item` . `nil`
if the user aborted the dialog.
vim:tw=78:ts=8:ft=help:norl:

View File

@ -116,42 +116,44 @@ M['textDocument/codeAction'] = function(_, result, ctx)
return
end
local option_strings = {"Code actions:"}
for i, action in ipairs(result) do
local title = action.title:gsub('\r\n', '\\r\\n')
title = title:gsub('\n', '\\n')
table.insert(option_strings, string.format("%d. %s", i, title))
end
local choice = vim.fn.inputlist(option_strings)
if choice < 1 or choice > #result then
return
end
local action = result[choice]
-- textDocument/codeAction can return either Command[] or CodeAction[]
--
-- CodeAction
-- ...
-- edit?: WorkspaceEdit -- <- must be applied before command
-- command?: Command
--
-- Command:
-- title: string
-- command: string
-- arguments?: any[]
--
if action.edit then
util.apply_workspace_edit(action.edit)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
local fn = vim.lsp.commands[command.command]
if fn then
fn(command, ctx)
else
buf.execute_command(command)
---@private
local function on_user_choice(action)
if not action then
return
end
-- textDocument/codeAction can return either Command[] or CodeAction[]
--
-- CodeAction
-- ...
-- edit?: WorkspaceEdit -- <- must be applied before command
-- command?: Command
--
-- Command:
-- title: string
-- command: string
-- arguments?: any[]
--
if action.edit then
util.apply_workspace_edit(action.edit)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
local fn = vim.lsp.commands[command.command]
if fn then
fn(command, ctx)
else
buf.execute_command(command)
end
end
end
vim.ui.select(result, {
prompt = 'Code actions:',
format_entry = function(action)
local title = action.title:gsub('\r\n', '\\r\\n')
return title:gsub('\n', '\\n')
end,
}, on_user_choice)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit

36
runtime/lua/vim/ui.lua Normal file
View File

@ -0,0 +1,36 @@
local M = {}
--- Prompts the user to pick a single item from a collection of entries
---
---@param items table Arbitrary items
---@param opts table Additional options
--- - prompt (string|nil)
--- Text of the prompt. Defaults to `Select one of:`
--- - format_item (function item -> text)
--- Function to format an
--- individual item from `items`. Defaults to `tostring`.
---@param on_choice function ((item|nil, idx|nil) -> ())
--- Called once the user made a choice.
--- `idx` is the 1-based index of `item` within `item`.
--- `nil` if the user aborted the dialog.
function M.select(items, opts, on_choice)
vim.validate {
items = { items, 'table', false },
on_choice = { on_choice, 'function', false },
}
opts = opts or {}
local choices = {opts.prompt or 'Select one of:'}
local format_entry = opts.format_entry or tostring
for i, item in pairs(items) do
table.insert(choices, string.format('%d: %s', i, format_entry(item)))
end
local choice = vim.fn.inputlist(choices)
if choice < 1 or choice > #items then
on_choice(nil, nil)
else
on_choice(items[choice], choice)
end
end
return M

View File

@ -123,11 +123,13 @@ CONFIG = {
'vim.lua',
'shared.lua',
'uri.lua',
'ui.lua',
],
'files': ' '.join([
os.path.join(base_dir, 'src/nvim/lua/vim.lua'),
os.path.join(base_dir, 'runtime/lua/vim/shared.lua'),
os.path.join(base_dir, 'runtime/lua/vim/uri.lua'),
os.path.join(base_dir, 'runtime/lua/vim/ui.lua'),
]),
'file_patterns': '*.lua',
'fn_name_prefix': '',
@ -141,6 +143,7 @@ CONFIG = {
# `shared` functions are exposed on the `vim` module.
'shared': 'vim',
'uri': 'vim',
'ui': 'vim.ui',
},
'append_only': [
'shared.lua',

View File

@ -108,6 +108,9 @@ setmetatable(vim, {
elseif key == 'diagnostic' then
t.diagnostic = require('vim.diagnostic')
return t.diagnostic
elseif key == 'ui' then
t.ui = require('vim.ui')
return t.ui
end
end
})

View File

@ -0,0 +1,46 @@
local helpers = require('test.functional.helpers')(after_each)
local eq = helpers.eq
local exec_lua = helpers.exec_lua
local clear = helpers.clear
describe('vim.ui', function()
before_each(function()
clear()
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_entry = 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)
end)