mirror of
https://github.com/neovim/neovim
synced 2025-07-18 02:01:46 +00:00
feat(lsp): add codeAction/resolve support (#15818)
Closes https://github.com/neovim/neovim/issues/15339 and https://github.com/neovim/neovim/issues/15828
This commit is contained in:
committed by
GitHub
parent
3507d58dfb
commit
ec4731d982
@ -450,6 +450,93 @@ function M.clear_references()
|
|||||||
util.buf_clear_references()
|
util.buf_clear_references()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
---@private
|
||||||
|
--
|
||||||
|
--- This is not public because the main extension point is
|
||||||
|
--- vim.ui.select which can be overridden independently.
|
||||||
|
---
|
||||||
|
--- Can't call/use vim.lsp.handlers['textDocument/codeAction'] because it expects
|
||||||
|
--- `(err, CodeAction[] | Command[], ctx)`, but we want to aggregate the results
|
||||||
|
--- 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 action_tuples = {}
|
||||||
|
for client_id, result in pairs(results) do
|
||||||
|
for _, action in pairs(result.result or {}) do
|
||||||
|
table.insert(action_tuples, { client_id, action })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #action_tuples == 0 then
|
||||||
|
vim.notify('No code actions available', vim.log.levels.INFO)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
local function apply_action(action, client)
|
||||||
|
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
|
||||||
|
local enriched_ctx = vim.deepcopy(ctx)
|
||||||
|
enriched_ctx.client_id = client.id
|
||||||
|
fn(command, ctx)
|
||||||
|
else
|
||||||
|
M.execute_command(command)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
local function on_user_choice(action_tuple)
|
||||||
|
if not action_tuple 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[]
|
||||||
|
--
|
||||||
|
local client = vim.lsp.get_client_by_id(action_tuple[1])
|
||||||
|
local action = action_tuple[2]
|
||||||
|
if not action.edit
|
||||||
|
and client
|
||||||
|
and type(client.resolved_capabilities.code_action) == 'table'
|
||||||
|
and client.resolved_capabilities.code_action.resolveProvider then
|
||||||
|
|
||||||
|
client.request('codeAction/resolve', action, function(err, resolved_action)
|
||||||
|
if err then
|
||||||
|
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
apply_action(resolved_action, client)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
apply_action(action, client)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.ui.select(action_tuples, {
|
||||||
|
prompt = 'Code actions:',
|
||||||
|
format_item = function(action_tuple)
|
||||||
|
local title = action_tuple[2].title:gsub('\r\n', '\\r\\n')
|
||||||
|
return title:gsub('\n', '\\n')
|
||||||
|
end,
|
||||||
|
}, on_user_choice)
|
||||||
|
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
|
||||||
@ -457,11 +544,7 @@ local function code_action_request(params)
|
|||||||
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)
|
||||||
local actions = {}
|
on_code_action_results(results, { bufnr = bufnr, method = method, params = params })
|
||||||
for _, r in pairs(results) do
|
|
||||||
vim.list_extend(actions, r.result or {})
|
|
||||||
end
|
|
||||||
vim.lsp.handlers[method](nil, actions, {bufnr=bufnr, method=method})
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ local protocol = require 'vim.lsp.protocol'
|
|||||||
local util = require 'vim.lsp.util'
|
local util = require 'vim.lsp.util'
|
||||||
local vim = vim
|
local vim = vim
|
||||||
local api = vim.api
|
local api = vim.api
|
||||||
local buf = require 'vim.lsp.buf'
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@ -109,53 +108,6 @@ M['client/registerCapability'] = function(_, _, ctx)
|
|||||||
return vim.NIL
|
return vim.NIL
|
||||||
end
|
end
|
||||||
|
|
||||||
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
|
|
||||||
M['textDocument/codeAction'] = function(_, result, ctx)
|
|
||||||
if result == nil or vim.tbl_isempty(result) then
|
|
||||||
print("No code actions available")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
---@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_item = 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
|
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
|
||||||
M['workspace/applyEdit'] = function(_, workspace_edit)
|
M['workspace/applyEdit'] = function(_, workspace_edit)
|
||||||
if not workspace_edit then return end
|
if not workspace_edit then return end
|
||||||
|
@ -645,6 +645,10 @@ function protocol.make_client_capabilities()
|
|||||||
end)();
|
end)();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
dataSupport = true;
|
||||||
|
resolveSupport = {
|
||||||
|
properties = { 'edit', }
|
||||||
|
};
|
||||||
};
|
};
|
||||||
completion = {
|
completion = {
|
||||||
dynamicRegistration = false;
|
dynamicRegistration = false;
|
||||||
|
@ -564,6 +564,35 @@ function tests.decode_nil()
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function tests.code_action_with_resolve()
|
||||||
|
skeleton {
|
||||||
|
on_init = function()
|
||||||
|
return {
|
||||||
|
capabilities = {
|
||||||
|
codeActionProvider = {
|
||||||
|
resolveProvider = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end;
|
||||||
|
body = function()
|
||||||
|
notify('start')
|
||||||
|
local cmd = {
|
||||||
|
title = 'Command 1',
|
||||||
|
command = 'dummy1'
|
||||||
|
}
|
||||||
|
expect_request('textDocument/codeAction', function()
|
||||||
|
return nil, { cmd, }
|
||||||
|
end)
|
||||||
|
expect_request('codeAction/resolve', function()
|
||||||
|
return nil, cmd
|
||||||
|
end)
|
||||||
|
notify('shutdown')
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
-- Tests will be indexed by TEST_NAME
|
-- Tests will be indexed by TEST_NAME
|
||||||
|
|
||||||
local kill_timer = vim.loop.new_timer()
|
local kill_timer = vim.loop.new_timer()
|
||||||
|
@ -2376,26 +2376,43 @@ describe('LSP', function()
|
|||||||
|
|
||||||
describe('vim.lsp.buf.code_action', function()
|
describe('vim.lsp.buf.code_action', function()
|
||||||
it('Calls client side command if available', function()
|
it('Calls client side command if available', function()
|
||||||
eq(1, exec_lua [[
|
local client
|
||||||
local dummy_calls = 0
|
local expected_handlers = {
|
||||||
vim.lsp.commands.dummy = function()
|
{NIL, {}, {method="shutdown", client_id=1}};
|
||||||
dummy_calls = dummy_calls + 1
|
{NIL, {}, {method="start", client_id=1}};
|
||||||
|
}
|
||||||
|
test_rpc_server {
|
||||||
|
test_name = 'code_action_with_resolve',
|
||||||
|
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['dummy1'] = function(cmd)
|
||||||
|
vim.lsp.commands['dummy2'] = function()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
|
||||||
|
vim.fn.inputlist = function()
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
vim.lsp.buf.code_action()
|
||||||
|
]])
|
||||||
|
elseif ctx.method == 'shutdown' then
|
||||||
|
eq('function', exec_lua[[return type(vim.lsp.commands['dummy2'])]])
|
||||||
|
client.stop()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
local actions = {
|
}
|
||||||
{
|
|
||||||
title = 'Dummy command',
|
|
||||||
command = 'dummy',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
-- inputlist would require input and block the test;
|
|
||||||
vim.fn.inputlist = function()
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
local params = {}
|
|
||||||
local handler = require'vim.lsp.handlers'['textDocument/codeAction']
|
|
||||||
handler(nil, actions, { method = 'textDocument/codeAction', params = params }, nil)
|
|
||||||
return dummy_calls
|
|
||||||
]])
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
describe('vim.lsp.commands', function()
|
describe('vim.lsp.commands', function()
|
||||||
|
Reference in New Issue
Block a user