diff options
author | Mathias Fußenegger <mfussenegger@users.noreply.github.com> | 2021-09-28 23:04:01 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-28 14:04:01 -0700 |
commit | ec4731d982031e363a59efd4566fc72234bb43c8 (patch) | |
tree | c0f6dabcdd7c0ae86da4fd74da44dd80abe2dba3 | |
parent | 3507d58dfb87923aa4031cbefaf1ef576a45dcaf (diff) | |
download | rneovim-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.lua | 93 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 48 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 4 | ||||
-rw-r--r-- | test/functional/fixtures/fake-lsp-server.lua | 29 | ||||
-rw-r--r-- | test/functional/plugin/lsp_spec.lua | 55 |
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() |