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()* clear_references() *vim.lsp.buf.clear_references()*
Removes document highlights from current buffer. 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 Selects a code action available at the current cursor
position. position.
Parameters: ~ Parameters: ~
{context} table|nil `CodeActionContext` of the LSP specification: {options} table|nil Optional table which holds the
• diagnostics: (table|nil) LSP`Diagnostic[]` . Inferred from the current position if not following optional fields:
provided. • context (table|nil): Corresponds to `CodeActionContext` of the LSP specification:
• only: (string|nil) LSP `CodeActionKind` used • diagnostics (table|nil): LSP`Diagnostic[]` . Inferred from the current position if not
to filter the code actions. Most language provided.
servers support values like `refactor` or • only (string|nil): LSP `CodeActionKind`
`quickfix`. 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: ~ See also: ~
https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction 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 --- 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 --- need to be able to link a `CodeAction|Command` to the right client for
--- `codeAction/resolve` --- `codeAction/resolve`
local function on_code_action_results(results, ctx) local function on_code_action_results(results, ctx, options)
local action_tuples = {} local action_tuples = {}
local filter = options and options.filter
for client_id, result in pairs(results) do for client_id, result in pairs(results) do
for _, action in pairs(result.result or {}) 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
end end
if #action_tuples == 0 then if #action_tuples == 0 then
@ -557,6 +560,13 @@ local function on_code_action_results(results, ctx)
end end
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, { vim.ui.select(action_tuples, {
prompt = 'Code actions:', prompt = 'Code actions:',
kind = 'codeaction', kind = 'codeaction',
@ -571,35 +581,49 @@ end
--- Requests code actions from all clients and calls the handler exactly once --- Requests code actions from all clients and calls the handler exactly once
--- with all aggregated results --- with all aggregated results
---@private ---@private
local function code_action_request(params) local function code_action_request(params, options)
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local method = 'textDocument/codeAction' local method = 'textDocument/codeAction'
vim.lsp.buf_request_all(bufnr, method, params, function(results) 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)
end end
--- Selects a code action available at the current --- Selects a code action available at the current
--- cursor position. --- cursor position.
--- ---
---@param context table|nil `CodeActionContext` of the LSP specification: ---@param options table|nil Optional table which holds the following optional fields:
--- - diagnostics: (table|nil) --- - context (table|nil):
--- LSP `Diagnostic[]`. Inferred from the current --- Corresponds to `CodeActionContext` of the LSP specification:
--- position if not provided. --- - diagnostics (table|nil):
--- - only: (string|nil) --- LSP `Diagnostic[]`. Inferred from the current
--- LSP `CodeActionKind` used to filter the code actions. --- position if not provided.
--- Most language servers support values like `refactor` --- - only (string|nil):
--- or `quickfix`. --- 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 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
function M.code_action(context) function M.code_action(options)
validate { context = { context, 't', true } } validate { options = { options, 't', true } }
context = context or {} 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 if not context.diagnostics then
context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics() context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
end end
local params = util.make_range_params() local params = util.make_range_params()
params.context = context params.context = context
code_action_request(params) code_action_request(params, options)
end end
--- Performs |vim.lsp.buf.code_action()| for a given range. --- Performs |vim.lsp.buf.code_action()| for a given range.

View File

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

View File

@ -673,6 +673,36 @@ function tests.code_action_with_resolve()
} }
end 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() function tests.clientside_commands()
skeleton { skeleton {
on_init = function() on_init = function()

View File

@ -2665,6 +2665,42 @@ describe('LSP', function()
end end
} }
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) end)
describe('vim.lsp.commands', function() describe('vim.lsp.commands', function()
it('Accepts only string keys', function() it('Accepts only string keys', function()