Skip to content

Commit

Permalink
feat(lsp): add codeAction/resolve support (neovim#15818)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfussenegger committed Sep 28, 2021
1 parent 3507d58 commit ec4731d
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 72 deletions.
93 changes: 88 additions & 5 deletions runtime/lua/vim/lsp/buf.lua
Expand Up @@ -450,18 +450,101 @@ function M.clear_references()
util.buf_clear_references()
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
--- with all aggregated results
---@private
local function code_action_request(params)
local bufnr = vim.api.nvim_get_current_buf()
local method = 'textDocument/codeAction'
vim.lsp.buf_request_all(bufnr, method, params, function(results)
local actions = {}
for _, r in pairs(results) do
vim.list_extend(actions, r.result or {})
end
vim.lsp.handlers[method](nil, actions, {bufnr=bufnr, method=method})
on_code_action_results(results, { bufnr = bufnr, method = method, params = params })
end)
end

Expand Down
48 changes: 0 additions & 48 deletions runtime/lua/vim/lsp/handlers.lua
Expand Up @@ -3,7 +3,6 @@ local protocol = require 'vim.lsp.protocol'
local util = require 'vim.lsp.util'
local vim = vim
local api = vim.api
local buf = require 'vim.lsp.buf'

local M = {}

Expand Down Expand Up @@ -109,53 +108,6 @@ M['client/registerCapability'] = function(_, _, ctx)
return vim.NIL
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
M['workspace/applyEdit'] = function(_, workspace_edit)
if not workspace_edit then return end
Expand Down
4 changes: 4 additions & 0 deletions runtime/lua/vim/lsp/protocol.lua
Expand Up @@ -645,6 +645,10 @@ function protocol.make_client_capabilities()
end)();
};
};
dataSupport = true;
resolveSupport = {
properties = { 'edit', }
};
};
completion = {
dynamicRegistration = false;
Expand Down
29 changes: 29 additions & 0 deletions test/functional/fixtures/fake-lsp-server.lua
Expand Up @@ -564,6 +564,35 @@ function tests.decode_nil()
}
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

local kill_timer = vim.loop.new_timer()
Expand Down
55 changes: 36 additions & 19 deletions test/functional/plugin/lsp_spec.lua
Expand Up @@ -2376,26 +2376,43 @@ describe('LSP', function()

describe('vim.lsp.buf.code_action', function()
it('Calls client side command if available', function()
eq(1, exec_lua [[
local dummy_calls = 0
vim.lsp.commands.dummy = function()
dummy_calls = dummy_calls + 1
end
local actions = {
{
title = 'Dummy command',
command = 'dummy',
},
}
-- inputlist would require input and block the test;
vim.fn.inputlist = function()
return 1
local client
local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=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
local params = {}
local handler = require'vim.lsp.handlers'['textDocument/codeAction']
handler(nil, actions, { method = 'textDocument/codeAction', params = params }, nil)
return dummy_calls
]])
}
end)
end)
describe('vim.lsp.commands', function()
Expand Down

0 comments on commit ec4731d

Please sign in to comment.