feat(lsp): options to filter and auto-apply code actions (#18221)

Implement two new options to vim.lsp.buf.code_action():
 - filter (function): predicate taking an Action as input, and returning
   a boolean.
 - apply (boolean): when set to true, and there is just one remaining
   action (after filtering), the action is applied without user query.

These options can, for example, be used to filter out, and automatically
apply, the action indicated by the server to be preferred:

    vim.lsp.buf.code_action({
        filter = function(action)
            return action.isPreferred
        end,
        apply = true,
    })

Fix #17514.
This commit is contained in:
Fredrik Ekre 2022-04-30 10:14:31 +02:00 committed by GitHub
parent de2232878f
commit df09e03cf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 24 deletions

View File

@ -969,18 +969,28 @@ add_workspace_folder({workspace_folder})
clear_references() *vim.lsp.buf.clear_references()*
Removes document highlights from current buffer.
code_action({context}) *vim.lsp.buf.code_action()*
code_action({options}) *vim.lsp.buf.code_action()*
Selects a code action available at the current cursor
position.
Parameters: ~
{context} table|nil `CodeActionContext` of the LSP specification:
• diagnostics: (table|nil) LSP`Diagnostic[]` . Inferred from the current position if not
provided.
• only: (string|nil) LSP `CodeActionKind` used
to filter the code actions. Most language
servers support values like `refactor` or
`quickfix`.
{options} table|nil Optional table which holds the
following optional fields:
• context (table|nil): Corresponds to `CodeActionContext` of the LSP specification:
• diagnostics (table|nil): LSP`Diagnostic[]` . Inferred from the current position if not
provided.
• only (string|nil): LSP `CodeActionKind`
used to filter the code actions. Most
language servers support values like
`refactor` or `quickfix`.
• filter (function|nil): Predicate function
taking an `CodeAction` and returning a
boolean.
• apply (boolean|nil): When set to `true`, and
there is just one remaining action (after
filtering), the action is applied without
user query.
See also: ~
https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction

View File

@ -491,11 +491,14 @@ end
--- from multiple clients to have 1 single UI prompt for the user, yet we still
--- need to be able to link a `CodeAction|Command` to the right client for
--- `codeAction/resolve`
local function on_code_action_results(results, ctx)
local function on_code_action_results(results, ctx, options)
local action_tuples = {}
local filter = options and options.filter
for client_id, result in pairs(results) do
for _, action in pairs(result.result or {}) do
table.insert(action_tuples, { client_id, action })
if not filter or filter(action) then
table.insert(action_tuples, { client_id, action })
end
end
end
if #action_tuples == 0 then
@ -557,6 +560,13 @@ local function on_code_action_results(results, ctx)
end
end
-- If options.apply is given, and there are just one remaining code action,
-- apply it directly without querying the user.
if options and options.apply and #action_tuples == 1 then
on_user_choice(action_tuples[1])
return
end
vim.ui.select(action_tuples, {
prompt = 'Code actions:',
kind = 'codeaction',
@ -571,35 +581,49 @@ end
--- Requests code actions from all clients and calls the handler exactly once
--- with all aggregated results
---@private
local function code_action_request(params)
local function code_action_request(params, options)
local bufnr = vim.api.nvim_get_current_buf()
local method = 'textDocument/codeAction'
vim.lsp.buf_request_all(bufnr, method, params, function(results)
on_code_action_results(results, { bufnr = bufnr, method = method, params = params })
local ctx = { bufnr = bufnr, method = method, params = params}
on_code_action_results(results, ctx, options)
end)
end
--- Selects a code action available at the current
--- cursor position.
---
---@param context table|nil `CodeActionContext` of the LSP specification:
--- - diagnostics: (table|nil)
--- LSP `Diagnostic[]`. Inferred from the current
--- position if not provided.
--- - only: (string|nil)
--- LSP `CodeActionKind` used to filter the code actions.
--- Most language servers support values like `refactor`
--- or `quickfix`.
---@param options table|nil Optional table which holds the following optional fields:
--- - context (table|nil):
--- Corresponds to `CodeActionContext` of the LSP specification:
--- - diagnostics (table|nil):
--- LSP `Diagnostic[]`. Inferred from the current
--- position if not provided.
--- - only (string|nil):
--- LSP `CodeActionKind` used to filter the code actions.
--- Most language servers support values like `refactor`
--- or `quickfix`.
--- - filter (function|nil):
--- Predicate function taking an `CodeAction` and returning a boolean.
--- - apply (boolean|nil):
--- When set to `true`, and there is just one remaining action
--- (after filtering), the action is applied without user query.
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
function M.code_action(context)
validate { context = { context, 't', true } }
context = context or {}
function M.code_action(options)
validate { options = { options, 't', true } }
options = options or {}
-- Detect old API call code_action(context) which should now be
-- code_action({ context = context} )
if options.diagnostics or options.only then
options = { options = options }
end
local context = options.context or {}
if not context.diagnostics then
context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
end
local params = util.make_range_params()
params.context = context
code_action_request(params)
code_action_request(params, options)
end
--- Performs |vim.lsp.buf.code_action()| for a given range.

View File

@ -645,6 +645,7 @@ function protocol.make_client_capabilities()
end)();
};
};
isPreferredSupport = true;
dataSupport = true;
resolveSupport = {
properties = { 'edit', }

View File

@ -673,6 +673,36 @@ function tests.code_action_with_resolve()
}
end
function tests.code_action_filter()
skeleton {
on_init = function()
return {
capabilities = {
codeActionProvider = {
resolveProvider = false
}
}
}
end;
body = function()
notify('start')
local action = {
title = 'Action 1',
command = 'command'
}
local preferred_action = {
title = 'Action 2',
isPreferred = true,
command = 'preferred_command',
}
expect_request('textDocument/codeAction', function()
return nil, { action, preferred_action, }
end)
notify('shutdown')
end;
}
end
function tests.clientside_commands()
skeleton {
on_init = function()

View File

@ -2665,6 +2665,42 @@ describe('LSP', function()
end
}
end)
it('Filters and automatically applies action if requested', function()
local client
local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
test_rpc_server {
test_name = 'code_action_filter',
on_init = function(client_)
client = client_
end,
on_setup = function()
end,
on_exit = function(code, signal)
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end,
on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, result, ctx})
if ctx.method == 'start' then
exec_lua([[
vim.lsp.commands['preferred_command'] = function(cmd)
vim.lsp.commands['executed_preferred'] = function()
end
end
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
vim.lsp.buf.code_action({ filter = function(a) return a.isPreferred end, apply = true, })
]])
elseif ctx.method == 'shutdown' then
eq('function', exec_lua[[return type(vim.lsp.commands['executed_preferred'])]])
client.stop()
end
end
}
end)
end)
describe('vim.lsp.commands', function()
it('Accepts only string keys', function()