From ce678043e3461d45f1251979cf047dd529b117ea Mon Sep 17 00:00:00 2001 From: Jordan <46637683+JordanllHarper@users.noreply.github.com> Date: Thu, 17 Oct 2024 09:35:19 +0100 Subject: feat(lsp): show server name in code actions #30830 Problem: If there are multiple LSP clients, it's not clear which actions are provided by which servers. #30710 Solution: Show the server name next to each action (only if there are multiple clients). --- runtime/lua/vim/lsp/buf.lua | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 301c1f0cb6..984222efbe 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -827,11 +827,19 @@ local function on_code_action_results(results, opts) return end - ---@param item {action: lsp.Command|lsp.CodeAction} + ---@param item {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext} local function format_item(item) - local title = item.action.title:gsub('\r\n', '\\r\\n') - return title:gsub('\n', '\\n') + local clients = vim.lsp.get_clients({ bufnr = item.ctx.bufnr }) + local title = item.action.title:gsub('\r\n', '\\r\\n'):gsub('\n', '\\n') + + if #clients == 1 then + return title + end + + local source = vim.lsp.get_client_by_id(item.ctx.client_id).name + return ('%s [%s]'):format(title, source) end + local select_opts = { prompt = 'Code actions:', kind = 'codeaction', -- cgit From acbc6a7f91d15fe5f59df08227d196156fafedb4 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 13:17:10 +0100 Subject: fix(lsp.util): inconsistent handling of offset_encoding --- runtime/lua/vim/lsp/buf.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 301c1f0cb6..e3f4beeeb1 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -338,6 +338,8 @@ function M.rename(new_name, opts) -- Compute early to account for cursor movements after going async local cword = vim.fn.expand('') + --- @param range lsp.Range + --- @param offset_encoding string local function get_text_at_range(range, offset_encoding) return api.nvim_buf_get_text( bufnr, -- cgit From 3f3e4837d5f7d2d9cb1c89bd3a5b2ee8a730772a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 17:03:48 +0100 Subject: perf(validate): use lighter version - Also fix `vim.validate()` for PUC Lua when showing errors for values that aren't string or number. --- runtime/lua/vim/lsp/buf.lua | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index ccef314d86..f6837a627f 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -21,10 +21,8 @@ local M = {} --- ---@see |vim.lsp.buf_request()| local function request(method, params, handler) - validate({ - method = { method, 's' }, - handler = { handler, 'f', true }, - }) + validate('method', method, 'string') + validate('handler', handler, 'function', true) return vim.lsp.buf_request(0, method, params, handler) end @@ -439,7 +437,7 @@ end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references ---@param opts? vim.lsp.ListOpts function M.references(context, opts) - validate({ context = { context, 't', true } }) + validate('context', context, 'table', true) local params = util.make_position_params() params.context = context or { includeDeclaration = true, @@ -857,7 +855,7 @@ end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction ---@see vim.lsp.protocol.CodeActionTriggerKind function M.code_action(opts) - validate({ options = { opts, 't', true } }) + validate('options', opts, 'table', true) opts = opts or {} -- Detect old API call code_action(context) which should now be -- code_action({ context = context} ) @@ -935,10 +933,8 @@ end --- @param command_params lsp.ExecuteCommandParams --- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand function M.execute_command(command_params) - validate({ - command = { command_params.command, 's' }, - arguments = { command_params.arguments, 't', true }, - }) + validate('command', command_params.command, 'string') + validate('arguments', command_params.arguments, 'table', true) command_params = { command = command_params.command, arguments = command_params.arguments, -- cgit From dff684fdb3d2e787ac6d6fd49ec52ede604fd0ce Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Tue, 8 Oct 2024 19:04:28 +0200 Subject: feat(lsp)!: support multiple clients in lsp.buf.references Relates to: - https://github.com/neovim/neovim/issues/17712 - https://github.com/neovim/neovim/issues/30034 --- runtime/lua/vim/lsp/buf.lua | 58 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index f6837a627f..8803c0495a 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -438,11 +438,59 @@ end ---@param opts? vim.lsp.ListOpts function M.references(context, opts) validate('context', context, 'table', true) - local params = util.make_position_params() - params.context = context or { - includeDeclaration = true, - } - request_with_opts(ms.textDocument_references, params, opts) + local clients = vim.lsp.get_clients({ method = ms.textDocument_references }) + if not next(clients) then + return + end + local win = api.nvim_get_current_win() + local bufnr = api.nvim_get_current_buf() + opts = opts or {} + + local all_items = {} + local title = 'References' + + local function on_done() + if not next(all_items) then + vim.notify('No references found') + else + local list = { + title = title, + items = all_items, + context = { + method = ms.textDocument_references, + bufnr = bufnr, + }, + } + if opts.loclist then + vim.fn.setloclist(0, {}, ' ', list) + vim.cmd.lopen() + elseif opts.on_list then + assert(vim.is_callable(opts.on_list), 'on_list is not a function') + opts.on_list(list) + else + vim.fn.setqflist({}, ' ', list) + vim.cmd('botright copen') + end + end + end + + local remaining = #clients + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + + ---@diagnostic disable-next-line: inject-field + params.context = context or { + includeDeclaration = true, + } + client.request(ms.textDocument_references, params, function(_, result) + local items = util.locations_to_items(result or {}, client.offset_encoding) + vim.list_extend(all_items, items) + remaining = remaining - 1 + if remaining == 0 then + on_done() + end + end) + end end --- Lists all symbols in the current buffer in the quickfix window. -- cgit From 0083e03d6fa7586d0d6360b40b52b0cab0d2e7ba Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sun, 20 Oct 2024 23:40:44 +0200 Subject: feat(lsp)!: support multiple clients in goto methods (#30877) Relates to: - https://github.com/neovim/neovim/issues/30034 - https://github.com/neovim/neovim/issues/17712 - https://github.com/neovim/neovim/issues/16363 Closes: - https://github.com/neovim/neovim/issues/26936 (but only provides bufnr and method) - https://github.com/neovim/neovim/issues/22318 Might fix: https://github.com/neovim/neovim/issues/30737 --- runtime/lua/vim/lsp/buf.lua | 89 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 8803c0495a..7bf3565499 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -50,6 +50,79 @@ local function request_with_opts(name, params, opts) request(name, params, req_handler) end +---@param method string +---@param opts? vim.lsp.LocationOpts +local function get_locations(method, opts) + opts = opts or {} + local bufnr = api.nvim_get_current_buf() + local clients = vim.lsp.get_clients({ method = method, bufnr = bufnr }) + if not next(clients) then + vim.notify(vim.lsp._unsupported_method(method), vim.log.levels.WARN) + return + end + local win = api.nvim_get_current_win() + local remaining = #clients + + ---@type vim.quickfix.entry[] + local all_items = {} + + ---@param result nil|lsp.Location|lsp.Location[] + ---@param client vim.lsp.Client + local function on_response(_, result, client) + local locations = {} + if result then + locations = vim.islist(result) and result or { result } + end + local items = util.locations_to_items(locations, client.offset_encoding) + vim.list_extend(all_items, items) + remaining = remaining - 1 + if remaining == 0 then + if vim.tbl_isempty(all_items) then + vim.notify('No locations found', vim.log.levels.INFO) + return + end + + local title = 'LSP locations' + if opts.on_list then + assert(vim.is_callable(opts.on_list), 'on_list is not a function') + opts.on_list({ + title = title, + items = all_items, + context = { bufnr = bufnr, method = method }, + }) + return + end + + if #all_items == 1 then + local item = all_items[1] + local b = item.bufnr or vim.fn.bufadd(item.filename) + vim.bo[b].buflisted = true + local w = opts.reuse_win and vim.fn.win_findbuf(b)[1] or win + api.nvim_win_set_buf(w, b) + api.nvim_win_set_cursor(w, { item.lnum, item.col - 1 }) + vim._with({ win = w }, function() + -- Open folds under the cursor + vim.cmd('normal! zv') + end) + return + end + if opts.loclist then + vim.fn.setloclist(0, {}, ' ', { title = title, items = all_items }) + vim.cmd.lopen() + else + vim.fn.setqflist({}, ' ', { title = title, items = all_items }) + vim.cmd('botright copen') + end + end + end + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + client.request(method, params, function(_, result) + on_response(_, result, client) + end) + end +end + --- @class vim.lsp.ListOpts --- --- list-handler replacing the default handler. @@ -87,30 +160,26 @@ end --- @note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead. --- @param opts? vim.lsp.LocationOpts function M.declaration(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_declaration, params, opts) + get_locations(ms.textDocument_declaration, opts) end --- Jumps to the definition of the symbol under the cursor. --- @param opts? vim.lsp.LocationOpts function M.definition(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_definition, params, opts) + get_locations(ms.textDocument_definition, opts) end --- Jumps to the definition of the type of the symbol under the cursor. --- @param opts? vim.lsp.LocationOpts function M.type_definition(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_typeDefinition, params, opts) + get_locations(ms.textDocument_typeDefinition, opts) end --- Lists all the implementations for the symbol under the cursor in the --- quickfix window. --- @param opts? vim.lsp.LocationOpts function M.implementation(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_implementation, params, opts) + get_locations(ms.textDocument_implementation, opts) end --- Displays signature information about the symbol under the cursor in a @@ -438,12 +507,12 @@ end ---@param opts? vim.lsp.ListOpts function M.references(context, opts) validate('context', context, 'table', true) - local clients = vim.lsp.get_clients({ method = ms.textDocument_references }) + local bufnr = api.nvim_get_current_buf() + local clients = vim.lsp.get_clients({ method = ms.textDocument_references, bufnr = bufnr }) if not next(clients) then return end local win = api.nvim_get_current_win() - local bufnr = api.nvim_get_current_buf() opts = opts or {} local all_items = {} -- cgit From 629a5b71b55e439392e2a5791940f6dc8d0dd7c0 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 22 Oct 2024 12:16:24 +0100 Subject: fix(lsp): support multiple clients in typehierarchy --- runtime/lua/vim/lsp/buf.lua | 139 +++++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 67 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 7bf3565499..fc41246588 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -569,6 +569,23 @@ function M.document_symbol(opts) request_with_opts(ms.textDocument_documentSymbol, params, opts) end +--- @param client_id integer +--- @param method string +--- @param params table +--- @param handler? lsp.Handler +--- @param bufnr? integer +local function request_with_id(client_id, method, params, handler, bufnr) + local client = vim.lsp.get_client_by_id(client_id) + if not client then + vim.notify( + string.format('Client with id=%d disappeared during call hierarchy request', client_id), + vim.log.levels.WARN + ) + return + end + client.request(method, params, handler, bufnr) +end + --- @param call_hierarchy_items lsp.CallHierarchyItem[] --- @return lsp.CallHierarchyItem? local function pick_call_hierarchy_item(call_hierarchy_items) @@ -600,19 +617,11 @@ local function call_hierarchy(method) vim.notify('No item resolved', vim.log.levels.WARN) return end - local call_hierarchy_item = pick_call_hierarchy_item(result) - if not call_hierarchy_item then + local item = pick_call_hierarchy_item(result) + if not item then return end - local client = vim.lsp.get_client_by_id(ctx.client_id) - if client then - client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr) - else - vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', ctx.client_id), - vim.log.levels.WARN - ) - end + request_with_id(ctx.client_id, method, { item = item }, nil, ctx.bufnr) end) end @@ -630,78 +639,74 @@ function M.outgoing_calls() call_hierarchy(ms.callHierarchy_outgoingCalls) end +--- @param item lsp.TypeHierarchyItem +local function format_type_hierarchy_item(item) + if not item.detail or #item.detail == 0 then + return item.name + end + return string.format('%s %s', item.name, item.detail) +end + --- Lists all the subtypes or supertypes of the symbol under the --- cursor in the |quickfix| window. If the symbol can resolve to --- multiple items, the user can pick one using |vim.ui.select()|. ---@param kind "subtypes"|"supertypes" function M.typehierarchy(kind) local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes - - --- Merge results from multiple clients into a single table. Client-ID is preserved. - --- - --- @param results table - --- @return [integer, lsp.TypeHierarchyItem][] - local function merge_results(results) - local merged_results = {} - for client_id, client_result in pairs(results) do - if client_result.error then - vim.notify(client_result.error.message, vim.log.levels.WARN) - elseif client_result.result then - for _, item in pairs(client_result.result) do - table.insert(merged_results, { client_id, item }) - end - end - end - return merged_results + local bufnr = api.nvim_get_current_buf() + local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method }) + if not next(clients) then + vim.notify(vim.lsp._unsupported_method(method), vim.log.levels.WARN) + return end - local bufnr = api.nvim_get_current_buf() - local params = util.make_position_params() - --- @param results table - vim.lsp.buf_request_all(bufnr, ms.textDocument_prepareTypeHierarchy, params, function(results) - local merged_results = merge_results(results) - if #merged_results == 0 then - vim.notify('No items resolved', vim.log.levels.INFO) - return - end + local win = api.nvim_get_current_win() - if #merged_results == 1 then - local item = merged_results[1] - local client = vim.lsp.get_client_by_id(item[1]) - if client then - client.request(method, { item = item[2] }, nil, bufnr) - else - vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', item[1]), - vim.log.levels.WARN - ) - end + --- @param results [integer, lsp.TypeHierarchyItem][] + local function on_response(results) + if #results == 0 then + vim.notify('No items resolved', vim.log.levels.INFO) + elseif #results == 1 then + local client_id, item = results[1][1], results[1][2] + request_with_id(client_id, method, { item = item }, nil, bufnr) else - local select_opts = { + vim.ui.select(results, { prompt = 'Select a type hierarchy item:', kind = 'typehierarchy', - format_item = function(item) - if not item[2].detail or #item[2].detail == 0 then - return item[2].name - end - return string.format('%s %s', item[2].name, item[2].detail) + format_item = function(x) + return format_type_hierarchy_item(x[2]) end, - } - - vim.ui.select(merged_results, select_opts, function(item) - local client = vim.lsp.get_client_by_id(item[1]) - if client then - --- @type lsp.TypeHierarchyItem - client.request(method, { item = item[2] }, nil, bufnr) - else - vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', item[1]), - vim.log.levels.WARN - ) + }, function(x) + if x then + local client_id, item = x[1], x[2] + request_with_id(client_id, method, { item = item }, nil, bufnr) end end) end - end) + end + + local results = {} --- @type [integer, lsp.TypeHierarchyItem][] + + local remaining = #clients + + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + client.request(method, params, function(err, result, ctx) + --- @cast result lsp.TypeHierarchyItem[]? + if err then + vim.notify(err.message, vim.log.levels.WARN) + elseif result then + for _, item in pairs(result) do + results[#results + 1] = { ctx.client_id, item } + end + end + + remaining = remaining - 1 + if remaining == 0 then + on_response(results) + end + end, bufnr) + end end --- List workspace folders. -- cgit From ad4e14c201c2f400e721362d8581f524fdad9038 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 09:55:23 +0100 Subject: refactor(lsp.buf): use alias for vim.lsp --- runtime/lua/vim/lsp/buf.lua | 53 +++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 26 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index fc41246588..0bb7904ca1 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,4 +1,5 @@ local api = vim.api +local lsp = vim.lsp local validate = vim.validate local util = require('vim.lsp.util') local npcall = vim.F.npcall @@ -23,7 +24,7 @@ local M = {} local function request(method, params, handler) validate('method', method, 'string') validate('handler', handler, 'function', true) - return vim.lsp.buf_request(0, method, params, handler) + return lsp.buf_request(0, method, params, handler) end --- Displays hover information about the symbol under the cursor in a floating @@ -35,15 +36,15 @@ end --- You can scroll the contents the same as you would any other buffer. function M.hover() local params = util.make_position_params() - request(ms.textDocument_hover, params) + lsp.buf_request(0, ms.textDocument_hover, params) end local function request_with_opts(name, params, opts) local req_handler --- @type function? if opts then req_handler = function(err, result, ctx, config) - local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - local handler = client.handlers[name] or vim.lsp.handlers[name] + local client = assert(lsp.get_client_by_id(ctx.client_id)) + local handler = client.handlers[name] or lsp.handlers[name] handler(err, result, ctx, vim.tbl_extend('force', config or {}, opts)) end end @@ -55,9 +56,9 @@ end local function get_locations(method, opts) opts = opts or {} local bufnr = api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ method = method, bufnr = bufnr }) + local clients = lsp.get_clients({ method = method, bufnr = bufnr }) if not next(clients) then - vim.notify(vim.lsp._unsupported_method(method), vim.log.levels.WARN) + vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) return end local win = api.nvim_get_current_win() @@ -307,7 +308,7 @@ function M.format(opts) method = ms.textDocument_formatting end - local clients = vim.lsp.get_clients({ + local clients = lsp.get_clients({ id = opts.id, bufnr = bufnr, name = opts.name, @@ -344,7 +345,7 @@ function M.format(opts) end local params = set_range(client, util.make_formatting_params(opts.formatting_options)) client.request(method, params, function(...) - local handler = client.handlers[method] or vim.lsp.handlers[method] + local handler = client.handlers[method] or lsp.handlers[method] handler(...) do_format(next(clients, idx)) end, bufnr) @@ -386,7 +387,7 @@ end function M.rename(new_name, opts) opts = opts or {} local bufnr = opts.bufnr or api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ + local clients = lsp.get_clients({ bufnr = bufnr, name = opts.name, -- Clients must at least support rename, prepareRename is optional @@ -428,7 +429,7 @@ function M.rename(new_name, opts) local params = util.make_position_params(win, client.offset_encoding) params.newName = name local handler = client.handlers[ms.textDocument_rename] - or vim.lsp.handlers[ms.textDocument_rename] + or lsp.handlers[ms.textDocument_rename] client.request(ms.textDocument_rename, params, function(...) handler(...) try_use_client(next(clients, idx)) @@ -508,7 +509,7 @@ end function M.references(context, opts) validate('context', context, 'table', true) local bufnr = api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ method = ms.textDocument_references, bufnr = bufnr }) + local clients = lsp.get_clients({ method = ms.textDocument_references, bufnr = bufnr }) if not next(clients) then return end @@ -575,7 +576,7 @@ end --- @param handler? lsp.Handler --- @param bufnr? integer local function request_with_id(client_id, method, params, handler, bufnr) - local client = vim.lsp.get_client_by_id(client_id) + local client = lsp.get_client_by_id(client_id) if not client then vim.notify( string.format('Client with id=%d disappeared during call hierarchy request', client_id), @@ -654,9 +655,9 @@ end function M.typehierarchy(kind) local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes local bufnr = api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method }) + local clients = lsp.get_clients({ bufnr = bufnr, method = method }) if not next(clients) then - vim.notify(vim.lsp._unsupported_method(method), vim.log.levels.WARN) + vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) return end @@ -713,7 +714,7 @@ end --- function M.list_workspace_folders() local workspace_folders = {} - for _, client in pairs(vim.lsp.get_clients({ bufnr = 0 })) do + for _, client in pairs(lsp.get_clients({ bufnr = 0 })) do for _, folder in pairs(client.workspace_folders or {}) do table.insert(workspace_folders, folder.name) end @@ -736,7 +737,7 @@ function M.add_workspace_folder(workspace_folder) return end local bufnr = api.nvim_get_current_buf() - for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do client:_add_workspace_folder(workspace_folder) end end @@ -753,7 +754,7 @@ function M.remove_workspace_folder(workspace_folder) return end local bufnr = api.nvim_get_current_buf() - for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do client:_remove_workspace_folder(workspace_folder) end print(workspace_folder, 'is not currently part of the workspace') @@ -916,7 +917,7 @@ local function on_code_action_results(results, opts) -- command: string -- arguments?: any[] -- - local client = assert(vim.lsp.get_client_by_id(choice.ctx.client_id)) + local client = assert(lsp.get_client_by_id(choice.ctx.client_id)) local action = choice.action local bufnr = assert(choice.ctx.bufnr, 'Must have buffer number') @@ -951,14 +952,14 @@ local function on_code_action_results(results, opts) ---@param item {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext} local function format_item(item) - local clients = vim.lsp.get_clients({ bufnr = item.ctx.bufnr }) + local clients = lsp.get_clients({ bufnr = item.ctx.bufnr }) local title = item.action.title:gsub('\r\n', '\\r\\n'):gsub('\n', '\\n') if #clients == 1 then return title end - local source = vim.lsp.get_client_by_id(item.ctx.client_id).name + local source = lsp.get_client_by_id(item.ctx.client_id).name return ('%s [%s]'):format(title, source) end @@ -987,16 +988,16 @@ function M.code_action(opts) end local context = opts.context and vim.deepcopy(opts.context) or {} if not context.triggerKind then - context.triggerKind = vim.lsp.protocol.CodeActionTriggerKind.Invoked + context.triggerKind = lsp.protocol.CodeActionTriggerKind.Invoked end local mode = api.nvim_get_mode().mode local bufnr = api.nvim_get_current_buf() local win = api.nvim_get_current_win() - local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_codeAction }) + local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_codeAction }) local remaining = #clients if remaining == 0 then - if next(vim.lsp.get_clients({ bufnr = bufnr })) then - vim.notify(vim.lsp._unsupported_method(ms.textDocument_codeAction), vim.log.levels.WARN) + if next(lsp.get_clients({ bufnr = bufnr })) then + vim.notify(lsp._unsupported_method(ms.textDocument_codeAction), vim.log.levels.WARN) end return end @@ -1033,8 +1034,8 @@ function M.code_action(opts) if context.diagnostics then params.context = context else - local ns_push = vim.lsp.diagnostic.get_namespace(client.id, false) - local ns_pull = vim.lsp.diagnostic.get_namespace(client.id, true) + local ns_push = lsp.diagnostic.get_namespace(client.id, false) + local ns_pull = lsp.diagnostic.get_namespace(client.id, true) local diagnostics = {} local lnum = api.nvim_win_get_cursor(0)[1] - 1 vim.list_extend(diagnostics, vim.diagnostic.get(bufnr, { namespace = ns_pull, lnum = lnum })) -- cgit From 2ee39b7eb46f091bf22dd1ba3066afff51139bdd Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 09:58:22 +0100 Subject: refactor(lsp.buf): remove buf_request wrapper --- runtime/lua/vim/lsp/buf.lua | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 0bb7904ca1..4a48172ab1 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -7,26 +7,6 @@ local ms = require('vim.lsp.protocol').Methods local M = {} ---- Sends an async request to all active clients attached to the current ---- buffer. ---- ----@param method (string) LSP method name ----@param params (table|nil) Parameters to send to the server ----@param handler lsp.Handler? See |lsp-handler|. Follows |lsp-handler-resolution| ---- ----@return table client_request_ids Map of client-id:request-id pairs ----for all successful requests. ----@return function _cancel_all_requests Function which can be used to ----cancel all the requests. You could instead ----iterate all clients and call their `cancel_request()` methods. ---- ----@see |vim.lsp.buf_request()| -local function request(method, params, handler) - validate('method', method, 'string') - validate('handler', handler, 'function', true) - return lsp.buf_request(0, method, params, handler) -end - --- Displays hover information about the symbol under the cursor in a floating --- window. The window will be dismissed on cursor move. --- Calling the function twice will jump into the floating window @@ -48,7 +28,7 @@ local function request_with_opts(name, params, opts) handler(err, result, ctx, vim.tbl_extend('force', config or {}, opts)) end end - request(name, params, req_handler) + lsp.buf_request(0, name, params, req_handler) end ---@param method string @@ -187,7 +167,7 @@ end --- floating window. function M.signature_help() local params = util.make_position_params() - request(ms.textDocument_signatureHelp, params) + lsp.buf_request(0, ms.textDocument_signatureHelp, params) end --- Retrieves the completion items at the current cursor position. Can only be @@ -201,7 +181,7 @@ end function M.completion(context) local params = util.make_position_params() params.context = context - return request(ms.textDocument_completion, params) + return lsp.buf_request(0, ms.textDocument_completion, params) end ---@param bufnr integer @@ -609,7 +589,7 @@ end local function call_hierarchy(method) local params = util.make_position_params() --- @param result lsp.CallHierarchyItem[]? - request(ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx) + lsp.buf_request(0, ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx) if err then vim.notify(err.message, vim.log.levels.WARN) return @@ -794,7 +774,7 @@ end --- |hl-LspReferenceWrite| function M.document_highlight() local params = util.make_position_params() - request(ms.textDocument_documentHighlight, params) + lsp.buf_request(0, ms.textDocument_documentHighlight, params) end --- Removes document highlights from current buffer. @@ -1063,7 +1043,7 @@ function M.execute_command(command_params) arguments = command_params.arguments, workDoneToken = command_params.workDoneToken, } - request(ms.workspace_executeCommand, command_params) + lsp.buf_request(0, ms.workspace_executeCommand, command_params) end return M -- cgit From 2dcbfe78fcec5f73ce061bb24b718187b9c6b134 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 10:18:36 +0100 Subject: fix(lsp.buf): use correct offset_encoding for all requests Problem: `lsp.buf_request` send the same params to all servers and many calls to this pass PositionalParams which depends on the clients offset_encoding. This can result with incorrect params being sent to a server. Solution: `lsp.buf_request` `params` argument can now be passed as a function which takes the client as the first argument. This is used in lsp/buf.lua to construct correct params for each client request. --- runtime/lua/vim/lsp/buf.lua | 68 ++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 25 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 4a48172ab1..80ba3c648f 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -7,6 +7,19 @@ local ms = require('vim.lsp.protocol').Methods local M = {} +--- @param params? table +--- @return fun(client: vim.lsp.Client): lsp.TextDocumentPositionParams +local function client_positional_params(params) + local win = api.nvim_get_current_win() + return function(client) + local ret = util.make_position_params(win, client.offset_encoding) + if params then + ret = vim.tbl_extend('force', ret, params) + end + return ret + end +end + --- Displays hover information about the symbol under the cursor in a floating --- window. The window will be dismissed on cursor move. --- Calling the function twice will jump into the floating window @@ -15,8 +28,7 @@ local M = {} --- except that "q" dismisses the window. --- You can scroll the contents the same as you would any other buffer. function M.hover() - local params = util.make_position_params() - lsp.buf_request(0, ms.textDocument_hover, params) + lsp.buf_request(0, ms.textDocument_hover, client_positional_params()) end local function request_with_opts(name, params, opts) @@ -166,8 +178,7 @@ end --- Displays signature information about the symbol under the cursor in a --- floating window. function M.signature_help() - local params = util.make_position_params() - lsp.buf_request(0, ms.textDocument_signatureHelp, params) + lsp.buf_request(0, ms.textDocument_signatureHelp, client_positional_params()) end --- Retrieves the completion items at the current cursor position. Can only be @@ -179,9 +190,13 @@ end --- ---@see vim.lsp.protocol.CompletionTriggerKind function M.completion(context) - local params = util.make_position_params() - params.context = context - return lsp.buf_request(0, ms.textDocument_completion, params) + return lsp.buf_request( + 0, + ms.textDocument_completion, + client_positional_params({ + context = context, + }) + ) end ---@param bufnr integer @@ -587,23 +602,27 @@ end --- @param method string local function call_hierarchy(method) - local params = util.make_position_params() - --- @param result lsp.CallHierarchyItem[]? - lsp.buf_request(0, ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx) - if err then - vim.notify(err.message, vim.log.levels.WARN) - return - end - if not result or vim.tbl_isempty(result) then - vim.notify('No item resolved', vim.log.levels.WARN) - return - end - local item = pick_call_hierarchy_item(result) - if not item then - return + lsp.buf_request( + 0, + ms.textDocument_prepareCallHierarchy, + client_positional_params(), + --- @param result lsp.CallHierarchyItem[]? + function(err, result, ctx) + if err then + vim.notify(err.message, vim.log.levels.WARN) + return + end + if not result or vim.tbl_isempty(result) then + vim.notify('No item resolved', vim.log.levels.WARN) + return + end + local item = pick_call_hierarchy_item(result) + if not item then + return + end + request_with_id(ctx.client_id, method, { item = item }, nil, ctx.bufnr) end - request_with_id(ctx.client_id, method, { item = item }, nil, ctx.bufnr) - end) + ) end --- Lists all the call sites of the symbol under the cursor in the @@ -773,8 +792,7 @@ end --- |hl-LspReferenceRead| --- |hl-LspReferenceWrite| function M.document_highlight() - local params = util.make_position_params() - lsp.buf_request(0, ms.textDocument_documentHighlight, params) + lsp.buf_request(0, ms.textDocument_documentHighlight, client_positional_params()) end --- Removes document highlights from current buffer. -- cgit From 3c51058d7611ef235a23ae77f994403480237700 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Thu, 24 Oct 2024 10:22:38 +0200 Subject: fix(lsp): set tagstack on jump via goto methods Follow up to https://github.com/neovim/neovim/pull/30877 Fixes https://github.com/neovim/neovim/issues/30926 --- runtime/lua/vim/lsp/buf.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 80ba3c648f..1858095918 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -54,6 +54,9 @@ local function get_locations(method, opts) return end local win = api.nvim_get_current_win() + local from = vim.fn.getpos('.') + from[1] = bufnr + local tagname = vim.fn.expand('') local remaining = #clients ---@type vim.quickfix.entry[] @@ -89,6 +92,13 @@ local function get_locations(method, opts) if #all_items == 1 then local item = all_items[1] local b = item.bufnr or vim.fn.bufadd(item.filename) + + -- Save position in jumplist + vim.cmd("normal! m'") + -- Push a new item into tagstack + local tagstack = { { tagname = tagname, from = from } } + vim.fn.settagstack(vim.fn.win_getid(win), { items = tagstack }, 't') + vim.bo[b].buflisted = true local w = opts.reuse_win and vim.fn.win_findbuf(b)[1] or win api.nvim_win_set_buf(w, b) -- cgit From c8d7d65679f832b406eb551411721cf21bae52bb Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 11:34:17 +0100 Subject: fix(lsp): use correct method for prepareTypehierarchy Regression from #30902 --- runtime/lua/vim/lsp/buf.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 1858095918..e0c03edf1e 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -663,8 +663,9 @@ end ---@param kind "subtypes"|"supertypes" function M.typehierarchy(kind) local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes + local pmethod = ms.textDocument_prepareTypeHierarchy local bufnr = api.nvim_get_current_buf() - local clients = lsp.get_clients({ bufnr = bufnr, method = method }) + local clients = lsp.get_clients({ bufnr = bufnr, method = pmethod }) if not next(clients) then vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) return @@ -701,7 +702,7 @@ function M.typehierarchy(kind) for _, client in ipairs(clients) do local params = util.make_position_params(win, client.offset_encoding) - client.request(method, params, function(err, result, ctx) + client.request(pmethod, params, function(err, result, ctx) --- @cast result lsp.TypeHierarchyItem[]? if err then vim.notify(err.message, vim.log.levels.WARN) -- cgit From 39d79efa1e1e1e5c3476dee54cc2bc4abc725a8f Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 11:20:27 +0100 Subject: fix(lsp): better multi-client support for callHierarchy Only ever display a dialogue box once. Switch from vim.fn.inpulist to vim.ui.select refactor(lsp): merge call and type hierarchy functions --- runtime/lua/vim/lsp/buf.lua | 130 ++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 77 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index e0c03edf1e..f2d5d204f4 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -584,7 +584,7 @@ local function request_with_id(client_id, method, params, handler, bufnr) local client = lsp.get_client_by_id(client_id) if not client then vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', client_id), + string.format('Client with id=%d disappeared during hierarchy request', client_id), vim.log.levels.WARN ) return @@ -592,80 +592,33 @@ local function request_with_id(client_id, method, params, handler, bufnr) client.request(method, params, handler, bufnr) end ---- @param call_hierarchy_items lsp.CallHierarchyItem[] ---- @return lsp.CallHierarchyItem? -local function pick_call_hierarchy_item(call_hierarchy_items) - if #call_hierarchy_items == 1 then - return call_hierarchy_items[1] - end - local items = {} - for i, item in pairs(call_hierarchy_items) do - local entry = item.detail or item.name - table.insert(items, string.format('%d. %s', i, entry)) - end - local choice = vim.fn.inputlist(items) - if choice < 1 or choice > #items then - return - end - return call_hierarchy_items[choice] -end - ---- @param method string -local function call_hierarchy(method) - lsp.buf_request( - 0, - ms.textDocument_prepareCallHierarchy, - client_positional_params(), - --- @param result lsp.CallHierarchyItem[]? - function(err, result, ctx) - if err then - vim.notify(err.message, vim.log.levels.WARN) - return - end - if not result or vim.tbl_isempty(result) then - vim.notify('No item resolved', vim.log.levels.WARN) - return - end - local item = pick_call_hierarchy_item(result) - if not item then - return - end - request_with_id(ctx.client_id, method, { item = item }, nil, ctx.bufnr) - end - ) -end - ---- Lists all the call sites of the symbol under the cursor in the ---- |quickfix| window. If the symbol can resolve to multiple ---- items, the user can pick one in the |inputlist()|. -function M.incoming_calls() - call_hierarchy(ms.callHierarchy_incomingCalls) -end - ---- Lists all the items that are called by the symbol under the ---- cursor in the |quickfix| window. If the symbol can resolve to ---- multiple items, the user can pick one in the |inputlist()|. -function M.outgoing_calls() - call_hierarchy(ms.callHierarchy_outgoingCalls) -end - ---- @param item lsp.TypeHierarchyItem -local function format_type_hierarchy_item(item) +--- @param item lsp.TypeHierarchyItem|lsp.CallHierarchyItem +local function format_hierarchy_item(item) if not item.detail or #item.detail == 0 then return item.name end return string.format('%s %s', item.name, item.detail) end ---- Lists all the subtypes or supertypes of the symbol under the ---- cursor in the |quickfix| window. If the symbol can resolve to ---- multiple items, the user can pick one using |vim.ui.select()|. ----@param kind "subtypes"|"supertypes" -function M.typehierarchy(kind) - local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes - local pmethod = ms.textDocument_prepareTypeHierarchy +local hierarchy_methods = { + [ms.typeHierarchy_subtypes] = 'type', + [ms.typeHierarchy_supertypes] = 'type', + [ms.callHierarchy_incomingCalls] = 'call', + [ms.callHierarchy_outgoingCalls] = 'call', +} + +--- @param method string +local function hierarchy(method) + local kind = hierarchy_methods[method] + if not kind then + error('unsupported method ' .. method) + end + + local prepare_method = kind == 'type' and ms.textDocument_prepareTypeHierarchy + or ms.textDocument_prepareCallHierarchy + local bufnr = api.nvim_get_current_buf() - local clients = lsp.get_clients({ bufnr = bufnr, method = pmethod }) + local clients = lsp.get_clients({ bufnr = bufnr, method = prepare_method }) if not next(clients) then vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) return @@ -673,19 +626,19 @@ function M.typehierarchy(kind) local win = api.nvim_get_current_win() - --- @param results [integer, lsp.TypeHierarchyItem][] + --- @param results [integer, lsp.TypeHierarchyItem|lsp.CallHierarchyItem][] local function on_response(results) if #results == 0 then - vim.notify('No items resolved', vim.log.levels.INFO) + vim.notify('No item resolved', vim.log.levels.WARN) elseif #results == 1 then local client_id, item = results[1][1], results[1][2] request_with_id(client_id, method, { item = item }, nil, bufnr) else vim.ui.select(results, { - prompt = 'Select a type hierarchy item:', - kind = 'typehierarchy', + prompt = string.format('Select a %s hierarchy item:', kind), + kind = kind .. 'hierarchy', format_item = function(x) - return format_type_hierarchy_item(x[2]) + return format_hierarchy_item(x[2]) end, }, function(x) if x then @@ -696,18 +649,18 @@ function M.typehierarchy(kind) end end - local results = {} --- @type [integer, lsp.TypeHierarchyItem][] + local results = {} --- @type [integer, lsp.TypeHierarchyItem|lsp.CallHierarchyItem][] local remaining = #clients for _, client in ipairs(clients) do local params = util.make_position_params(win, client.offset_encoding) - client.request(pmethod, params, function(err, result, ctx) - --- @cast result lsp.TypeHierarchyItem[]? + --- @param result lsp.CallHierarchyItem[]|lsp.TypeHierarchyItem[]? + client.request(prepare_method, params, function(err, result, ctx) if err then vim.notify(err.message, vim.log.levels.WARN) elseif result then - for _, item in pairs(result) do + for _, item in ipairs(result) do results[#results + 1] = { ctx.client_id, item } end end @@ -720,6 +673,29 @@ function M.typehierarchy(kind) end end +--- Lists all the call sites of the symbol under the cursor in the +--- |quickfix| window. If the symbol can resolve to multiple +--- items, the user can pick one in the |inputlist()|. +function M.incoming_calls() + hierarchy(ms.callHierarchy_incomingCalls) +end + +--- Lists all the items that are called by the symbol under the +--- cursor in the |quickfix| window. If the symbol can resolve to +--- multiple items, the user can pick one in the |inputlist()|. +function M.outgoing_calls() + hierarchy(ms.callHierarchy_outgoingCalls) +end + +--- Lists all the subtypes or supertypes of the symbol under the +--- cursor in the |quickfix| window. If the symbol can resolve to +--- multiple items, the user can pick one using |vim.ui.select()|. +---@param kind "subtypes"|"supertypes" +function M.typehierarchy(kind) + local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes + hierarchy(method) +end + --- List workspace folders. --- function M.list_workspace_folders() -- cgit From 7a7747f1e4d96aab53ff9c52d0c3492308c22c58 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 12:11:27 +0100 Subject: feat(lsp): deprecate execute_command with client:exec_cmd --- runtime/lua/vim/lsp/buf.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index f2d5d204f4..4cd4008105 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -881,7 +881,8 @@ local function on_code_action_results(results, opts) local a_cmd = action.command if a_cmd then local command = type(a_cmd) == 'table' and a_cmd or action - client:_exec_cmd(command, ctx) + --- @cast command lsp.Command + client:exec_cmd(command, ctx) end end @@ -1037,12 +1038,14 @@ function M.code_action(opts) end end +--- @deprecated --- Executes an LSP server command. --- @param command_params lsp.ExecuteCommandParams --- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand function M.execute_command(command_params) validate('command', command_params.command, 'string') validate('arguments', command_params.arguments, 'table', true) + vim.deprecate('execute_command', 'client:exec_cmd', '0.12') command_params = { command = command_params.command, arguments = command_params.arguments, -- cgit From 54249d051c3e0b97321939e6a3ae3d2e83971ce7 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 15:43:38 +0100 Subject: feat(lsp): deprecate vim.lsp.buf.completion Use `vim.lsp.completion.trigger()` instead' --- runtime/lua/vim/lsp/buf.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 4cd4008105..5f50e361c4 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -191,6 +191,7 @@ function M.signature_help() lsp.buf_request(0, ms.textDocument_signatureHelp, client_positional_params()) end +--- @deprecated --- Retrieves the completion items at the current cursor position. Can only be --- called in Insert mode. --- @@ -200,6 +201,7 @@ end --- ---@see vim.lsp.protocol.CompletionTriggerKind function M.completion(context) + vim.depends('vim.lsp.buf.completion', 'vim.lsp.commpletion.trigger', '0.12') return lsp.buf_request( 0, ms.textDocument_completion, -- cgit From 8260e4860b27a54a061bd8e2a9da23069993953a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 16:47:41 +0100 Subject: feat(lsp)!: multiple client support for vim.lsp.buf.hover() Deprecate `vim.lsp.handlers.hover` and `vim.lsp.handlers['textDocument/hover']` --- runtime/lua/vim/lsp/buf.lua | 77 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 5f50e361c4..dd06b1b6c0 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -20,6 +20,9 @@ local function client_positional_params(params) end end +--- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts +--- @field silent? boolean + --- Displays hover information about the symbol under the cursor in a floating --- window. The window will be dismissed on cursor move. --- Calling the function twice will jump into the floating window @@ -27,8 +30,78 @@ end --- In the floating window, all commands and mappings are available as usual, --- except that "q" dismisses the window. --- You can scroll the contents the same as you would any other buffer. -function M.hover() - lsp.buf_request(0, ms.textDocument_hover, client_positional_params()) +--- @param config? vim.lsp.buf.hover.Opts +function M.hover(config) + config = config or {} + config.focus_id = ms.textDocument_hover + + lsp.buf_request_all(0, ms.textDocument_hover, client_positional_params(), function(results, ctx) + if api.nvim_get_current_buf() ~= ctx.bufnr then + -- Ignore result since buffer changed. This happens for slow language servers. + return + end + + -- Filter errors from results + local results1 = {} --- @type table + + for client_id, resp in pairs(results) do + local err, result = resp.err, resp.result + if err then + lsp.log.error(err.code, err.message) + elseif result then + results1[client_id] = result + end + end + + if #results1 == 0 then + if config.silent ~= true then + vim.notify('No information available') + end + return + end + + local contents = {} --- @type string[] + + local nresults = #vim.tbl_keys(results1) + + local format = 'markdown' + + for client_id, result in pairs(results1) do + if nresults > 1 then + -- Show client name if there are multiple clients + contents[#contents + 1] = string.format('# %s', lsp.get_client_by_id(client_id).name) + end + if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then + if #results1 == 1 then + format = 'plaintext' + contents = vim.split(result.contents.value or '', '\n', { trimempty = true }) + else + -- Surround plaintext with ``` to get correct formatting + contents[#contents + 1] = '```' + vim.list_extend( + contents, + vim.split(result.contents.value or '', '\n', { trimempty = true }) + ) + contents[#contents + 1] = '```' + end + else + vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents)) + end + contents[#contents + 1] = '---' + end + + -- Remove last linebreak ('---') + contents[#contents] = nil + + if vim.tbl_isempty(contents) then + if config.silent ~= true then + vim.notify('No information available') + end + return + end + + lsp.util.open_floating_preview(contents, format, config) + end) end local function request_with_opts(name, params, opts) -- cgit From b4599acbf8e30aa1234bc4041546f960dcc3a238 Mon Sep 17 00:00:00 2001 From: notomo <18519692+notomo@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:57:33 +0900 Subject: fix(lsp): correct hover result handling (#30995) Problem: vim.lsp.buf.hover() displays "No information available" when client_id is not 1. Solution: use vim.tbl_isempty(tbl) instead of #tbl==0 --- runtime/lua/vim/lsp/buf.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index dd06b1b6c0..4818359fb3 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -53,7 +53,7 @@ function M.hover(config) end end - if #results1 == 0 then + if vim.tbl_isempty(results1) then if config.silent ~= true then vim.notify('No information available') end -- cgit From 9b357e30fdd0a575480182872331fdb87e9cc331 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 29 Oct 2024 09:36:02 +0000 Subject: feat(lsp)!: remove client-server handlers from vim.lsp.handlers - Partition the handlers in vim.lsp.handlers as: - client to server response handlers (RCS) - server to client request handlers (RSC) - server to client notification handlers (NSC) Note use string indexes instead of protocol.methods for improved typing in LuaLS (tip: use hover on RCS, RSC or NSC). --- runtime/lua/vim/lsp/buf.lua | 66 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 4818359fb3..152226a757 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -258,10 +258,72 @@ function M.implementation(opts) get_locations(ms.textDocument_implementation, opts) end +local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') + +--- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts +--- @field silent? boolean + +-- TODO(lewis6991): support multiple clients --- Displays signature information about the symbol under the cursor in a --- floating window. -function M.signature_help() - lsp.buf_request(0, ms.textDocument_signatureHelp, client_positional_params()) +--- @param config? vim.lsp.buf.signature_help.Opts +function M.signature_help(config) + local method = ms.textDocument_signatureHelp + + config = config or {} + config.focus_id = method + + lsp.buf_request(0, method, client_positional_params(), function(err, result, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + + if err then + vim.notify( + client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, + vim.log.levels.ERROR + ) + api.nvim_command('redraw') + return + end + + if api.nvim_get_current_buf() ~= ctx.bufnr then + -- Ignore result since buffer changed. This happens for slow language servers. + return + end + + -- When use `autocmd CompleteDone lua vim.lsp.buf.signature_help()` to call signatureHelp handler + -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `` to ignore + if not result or not result.signatures or not result.signatures[1] then + if config.silent ~= true then + print('No signature help available') + end + return + end + + local triggers = + vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') + + local ft = vim.bo[ctx.bufnr].filetype + local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers) + if not lines or vim.tbl_isempty(lines) then + if config.silent ~= true then + print('No signature help available') + end + return + end + + local fbuf = util.open_floating_preview(lines, 'markdown', config) + + -- Highlight the active parameter. + if hl then + vim.hl.range( + fbuf, + sig_help_ns, + 'LspSignatureActiveParameter', + { hl[1], hl[2] }, + { hl[3], hl[4] } + ) + end + end) end --- @deprecated -- cgit From 6e68fed37441096bf9fd2aa27b9bf6e7d7eae550 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 1 Nov 2024 12:30:36 +0000 Subject: feat(lsp): multi-client support for signature_help Signatures can be cycled using `` when the user enters the floating window. --- runtime/lua/vim/lsp/buf.lua | 116 +++++++++++++++++++++++++++++++------------- 1 file changed, 82 insertions(+), 34 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 152226a757..6d7597c5ff 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -258,6 +258,33 @@ function M.implementation(opts) get_locations(ms.textDocument_implementation, opts) end +--- @param results table +local function process_signature_help_results(results) + local signatures = {} --- @type [vim.lsp.Client,lsp.SignatureInformation][] + + -- Pre-process results + for client_id, r in pairs(results) do + local err = r.err + local client = assert(lsp.get_client_by_id(client_id)) + if err then + vim.notify( + client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, + vim.log.levels.ERROR + ) + api.nvim_command('redraw') + else + local result = r.result --- @type lsp.SignatureHelp + if result and result.signatures and result.signatures[1] then + for _, sig in ipairs(result.signatures) do + signatures[#signatures + 1] = { client, sig } + end + end + end + end + + return signatures +end + local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') --- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts @@ -270,58 +297,79 @@ local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') function M.signature_help(config) local method = ms.textDocument_signatureHelp - config = config or {} + config = config and vim.deepcopy(config) or {} config.focus_id = method - lsp.buf_request(0, method, client_positional_params(), function(err, result, ctx) - local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - - if err then - vim.notify( - client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, - vim.log.levels.ERROR - ) - api.nvim_command('redraw') - return - end - + lsp.buf_request_all(0, method, client_positional_params(), function(results, ctx) if api.nvim_get_current_buf() ~= ctx.bufnr then -- Ignore result since buffer changed. This happens for slow language servers. return end - -- When use `autocmd CompleteDone lua vim.lsp.buf.signature_help()` to call signatureHelp handler - -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `` to ignore - if not result or not result.signatures or not result.signatures[1] then + local signatures = process_signature_help_results(results) + + if not next(signatures) then if config.silent ~= true then print('No signature help available') end return end - local triggers = - vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') - local ft = vim.bo[ctx.bufnr].filetype - local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers) - if not lines or vim.tbl_isempty(lines) then - if config.silent ~= true then - print('No signature help available') + local total = #signatures + local idx = 0 + + --- @param update_win? integer + local function show_signature(update_win) + idx = (idx % total) + 1 + local client, result = signatures[idx][1], signatures[idx][2] + --- @type string[]? + local triggers = + vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') + local lines, hl = + util.convert_signature_help_to_markdown_lines({ signatures = { result } }, ft, triggers) + if not lines then + return end - return + + local sfx = total > 1 and string.format(' (%d/%d) ( to cycle)', idx, total) or '' + local title = string.format('Signature Help: %s%s', client.name, sfx) + if config.border then + config.title = title + else + table.insert(lines, 1, '# ' .. title) + if hl then + hl[1] = hl[1] + 1 + hl[3] = hl[3] + 1 + end + end + + config._update_win = update_win + + local buf, win = util.open_floating_preview(lines, 'markdown', config) + + if hl then + vim.api.nvim_buf_clear_namespace(buf, sig_help_ns, 0, -1) + vim.hl.range( + buf, + sig_help_ns, + 'LspSignatureActiveParameter', + { hl[1], hl[2] }, + { hl[3], hl[4] } + ) + end + return buf, win end - local fbuf = util.open_floating_preview(lines, 'markdown', config) + local fbuf, fwin = show_signature() - -- Highlight the active parameter. - if hl then - vim.hl.range( - fbuf, - sig_help_ns, - 'LspSignatureActiveParameter', - { hl[1], hl[2] }, - { hl[3], hl[4] } - ) + if total > 1 then + vim.keymap.set('n', '', function() + show_signature(fwin) + end, { + buffer = fbuf, + desc = 'Cycle next signature', + }) end end) end -- cgit From 44229bb85b6cff00193164967126d85a7a785a7b Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sun, 17 Nov 2024 12:31:32 -0800 Subject: feat(lsp): highlight hover target/range #31110 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem:** Despite the LSP providing the option for language servers to specify a range with a hover response (for highlighting), Neovim does not give the option to highlight this range. **Solution:** Add an option to `buf.hover()` which causes this range to be highlighted. Co-authored-by: Mathias Fußenegger --- runtime/lua/vim/lsp/buf.lua | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 6d7597c5ff..a75e322e90 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -20,6 +20,8 @@ local function client_positional_params(params) end end +local hover_ns = api.nvim_create_namespace('vim_lsp_hover_range') + --- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts --- @field silent? boolean @@ -30,13 +32,24 @@ end --- In the floating window, all commands and mappings are available as usual, --- except that "q" dismisses the window. --- You can scroll the contents the same as you would any other buffer. +--- +--- Note: to disable hover highlights, add the following to your config: +--- +--- ```lua +--- vim.api.nvim_create_autocmd('ColorScheme', { +--- callback = function() +--- vim.api.nvim_set_hl(0, 'LspReferenceTarget', {}) +--- end, +--- }) +--- ``` --- @param config? vim.lsp.buf.hover.Opts function M.hover(config) config = config or {} config.focus_id = ms.textDocument_hover lsp.buf_request_all(0, ms.textDocument_hover, client_positional_params(), function(results, ctx) - if api.nvim_get_current_buf() ~= ctx.bufnr then + local bufnr = assert(ctx.bufnr) + if api.nvim_get_current_buf() ~= bufnr then -- Ignore result since buffer changed. This happens for slow language servers. return end @@ -67,9 +80,10 @@ function M.hover(config) local format = 'markdown' for client_id, result in pairs(results1) do + local client = assert(lsp.get_client_by_id(client_id)) if nresults > 1 then -- Show client name if there are multiple clients - contents[#contents + 1] = string.format('# %s', lsp.get_client_by_id(client_id).name) + contents[#contents + 1] = string.format('# %s', client.name) end if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then if #results1 == 1 then @@ -87,6 +101,22 @@ function M.hover(config) else vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents)) end + local range = result.range + if range then + local start = range.start + local end_ = range['end'] + local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding) + local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding) + + vim.hl.range( + bufnr, + hover_ns, + 'LspReferenceTarget', + { start.line, start_idx }, + { end_.line, end_idx }, + { priority = vim.hl.priorities.user } + ) + end contents[#contents + 1] = '---' end @@ -100,7 +130,16 @@ function M.hover(config) return end - lsp.util.open_floating_preview(contents, format, config) + local _, winid = lsp.util.open_floating_preview(contents, format, config) + + api.nvim_create_autocmd('WinClosed', { + pattern = tostring(winid), + once = true, + callback = function() + api.nvim_buf_clear_namespace(bufnr, hover_ns, 0, -1) + return true + end, + }) end) end -- cgit From 989a37a594649528f28432388c0e7e28e8be2753 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 5 Nov 2024 17:34:21 +0000 Subject: refactor(lsp): fold in dynamic_registration code into the client Problem: Capability register logic is spread across 3 files. Solution: - Consolidate (and simplify) logic into the client. - Teach client.supports_method about resolve methods --- runtime/lua/vim/lsp/buf.lua | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'runtime/lua/vim/lsp/buf.lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index a75e322e90..6383855a30 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1131,12 +1131,7 @@ local function on_code_action_results(results, opts) local action = choice.action local bufnr = assert(choice.ctx.bufnr, 'Must have buffer number') - local reg = client.dynamic_capabilities:get(ms.textDocument_codeAction, { bufnr = bufnr }) - - local supports_resolve = vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') - or client.supports_method(ms.codeAction_resolve) - - if not action.edit and client and supports_resolve then + if not action.edit and client.supports_method(ms.codeAction_resolve) then client.request(ms.codeAction_resolve, action, function(err, resolved_action) if err then if action.command then -- cgit