aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMathias Fußenegger <mfussenegger@users.noreply.github.com>2021-09-28 23:04:01 +0200
committerGitHub <noreply@github.com>2021-09-28 14:04:01 -0700
commitec4731d982031e363a59efd4566fc72234bb43c8 (patch)
treec0f6dabcdd7c0ae86da4fd74da44dd80abe2dba3
parent3507d58dfb87923aa4031cbefaf1ef576a45dcaf (diff)
downloadrneovim-ec4731d982031e363a59efd4566fc72234bb43c8.tar.gz
rneovim-ec4731d982031e363a59efd4566fc72234bb43c8.tar.bz2
rneovim-ec4731d982031e363a59efd4566fc72234bb43c8.zip
feat(lsp): add codeAction/resolve support (#15818)
Closes https://github.com/neovim/neovim/issues/15339 and https://github.com/neovim/neovim/issues/15828
-rw-r--r--runtime/lua/vim/lsp/buf.lua93
-rw-r--r--runtime/lua/vim/lsp/handlers.lua48
-rw-r--r--runtime/lua/vim/lsp/protocol.lua4
-rw-r--r--test/functional/fixtures/fake-lsp-server.lua29
-rw-r--r--test/functional/plugin/lsp_spec.lua55
5 files changed, 157 insertions, 72 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 3e6a5ae2f1..245f29943e 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -450,6 +450,93 @@ 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
@@ -457,11 +544,7 @@ 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
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index def83a7320..eff27807be 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -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 = {}
@@ -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
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 27703b4503..b3aa8b934f 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -645,6 +645,10 @@ function protocol.make_client_capabilities()
end)();
};
};
+ dataSupport = true;
+ resolveSupport = {
+ properties = { 'edit', }
+ };
};
completion = {
dynamicRegistration = false;
diff --git a/test/functional/fixtures/fake-lsp-server.lua b/test/functional/fixtures/fake-lsp-server.lua
index 297641849d..8e03d9a46e 100644
--- a/test/functional/fixtures/fake-lsp-server.lua
+++ b/test/functional/fixtures/fake-lsp-server.lua
@@ -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()
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index 27f2d2536f..572573a3a6 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -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()