From df726408d7043c618877118909f53a78b85eb2fd Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 22 Oct 2020 20:59:17 +0200 Subject: lsp: Fix "client has shut down" errors during initializing (#13103) Language servers can already send log messages to the client while the server is still being initialized. This currently leads to "client has shut down" messages which are confusing to the user as the server is properly starting. To fix this this changes the `get_client_by_id` method to also return a client if it is still initializing. --- runtime/lua/vim/lsp.lua | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 585528dd5a..95d9c585ee 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -397,9 +397,8 @@ end --@param trace: "off" | "messages" | "verbose" | nil passed directly to the language --- server in the initialize request. Invalid/empty values will default to "off" --- ---@returns Client id. |vim.lsp.get_client_by_id()| Note: client is only ---- available after it has been initialized, which may happen after a small ---- delay (or never if there is an error). Use `on_init` to do any actions once +--@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be +--- fully initialized. Use `on_init` to do any actions once --- the client has been initialized. function lsp.start_client(config) local cleaned_config = validate_client_config(config) @@ -910,14 +909,14 @@ function lsp.buf_is_attached(bufnr, client_id) return (all_buffer_active_clients[bufnr] or {})[client_id] == true end ---- Gets an active client by id, or nil if the id is invalid or the ---- client is not yet initialized. ---- +--- Gets a client by id, or nil if the id is invalid. +--- The returned client may not yet be fully initialized. +-- --@param client_id client id number --- --@returns |vim.lsp.client| object, or nil function lsp.get_client_by_id(client_id) - return active_clients[client_id] + return active_clients[client_id] or uninitialized_clients[client_id] end --- Stops a client(s). -- cgit From 6312792d8a6a7d293661d33d440343d4cc6e0e6e Mon Sep 17 00:00:00 2001 From: francisco souza <108725+fsouza@users.noreply.github.com> Date: Sun, 25 Oct 2020 00:28:15 -0400 Subject: lsp: only send buf requests to servers that support the request (#12764) Refactors how required capabilities are detected and validated, and make sure requests are only sent to clients that support it (and only fail if no clients support the provided method). The validation happens at the buf_request level, because we assume that if someone is sending the request directly through the client, they know what they're doing. Also, let unknown methods go through. This is extracted from #12518 and closes #12755. Co-authored-by: francisco souza --- runtime/lua/vim/lsp.lua | 95 ++++++++++++++++++++++++++-------------- runtime/lua/vim/lsp/protocol.lua | 14 ++++++ 2 files changed, 76 insertions(+), 33 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 95d9c585ee..fad213212a 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -25,6 +25,27 @@ local lsp = { -- format_rpc_error = lsp_rpc.format_rpc_error; } +-- maps request name to the required resolved_capability in the client. +lsp._request_name_to_capability = { + ['textDocument/hover'] = 'hover'; + ['textDocument/signatureHelp'] = 'signature_help'; + ['textDocument/definition'] = 'goto_definition'; + ['textDocument/implementation'] = 'implementation'; + ['textDocument/declaration'] = 'declaration'; + ['textDocument/typeDefinition'] = 'type_definition'; + ['textDocument/documentSymbol'] = 'document_symbol'; + ['textDocument/workspaceSymbol'] = 'workspace_symbol'; + ['textDocument/prepareCallHierarchy'] = 'call_hierarchy'; + ['textDocument/rename'] = 'rename'; + ['textDocument/codeAction'] = 'code_action'; + ['workspace/executeCommand'] = 'execute_command'; + ['textDocument/references'] = 'find_references'; + ['textDocument/rangeFormatting'] = 'document_range_formatting'; + ['textDocument/formatting'] = 'document_formatting'; + ['textDocument/completion'] = 'completion'; + ['textDocument/documentHighlight'] = 'document_highlight'; +} + -- TODO improve handling of scratch buffers with LSP attached. --@private @@ -50,6 +71,16 @@ local function resolve_bufnr(bufnr) return bufnr end +--@private +--- callback called by the client when trying to call a method that's not +--- supported in any of the servers registered for the current buffer. +--@param method (string) name of the method +function lsp._unsupported_method(method) + local msg = string.format("method %s is not supported by any of the servers registered for the current buffer", method) + log.warn(msg) + return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) +end + --@private --- Checks whether a given path is a directory. --- @@ -575,6 +606,15 @@ function lsp.start_client(config) -- These are the cleaned up capabilities we use for dynamically deciding -- when to send certain events to clients. client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities) + client.supports_method = function(method) + local required_capability = lsp._request_name_to_capability[method] + -- if we don't know about the method, assume that the client supports it. + if not required_capability then + return true + end + + return client.resolved_capabilities[required_capability] + end if config.on_init then local status, err = pcall(config.on_init, client, result) if not status then @@ -597,19 +637,6 @@ function lsp.start_client(config) end) end - --@private - --- Throws error for a method that is not supported by the current LSP - --- server. - --- - --@param method (string) an LSP method name not supported by the LSP server. - --@returns (error) a 'MethodNotFound' JSON-RPC error response. - local function unsupported_method(method) - local msg = "server doesn't support "..method - local _ = log.warn() and log.warn(msg) - err_message(msg) - return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) - end - --@private --- Sends a request to the server. --- @@ -637,20 +664,6 @@ function lsp.start_client(config) or error(string.format("not found: %q request callback for client %q.", method, client.name)) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr) - -- TODO keep these checks or just let it go anyway? - if (not client.resolved_capabilities.hover and method == 'textDocument/hover') - or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp') - or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') - or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') - or (not client.resolved_capabilities.declaration and method == 'textDocument/declaration') - or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition') - or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') - or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol') - or (not client.resolved_capabilities.call_hierarchy and method == 'textDocument/prepareCallHierarchy') - then - callback(unsupported_method(method), method, nil, client_id, bufnr) - return - end return rpc.request(method, params, function(err, result) callback(err, method, result, client_id, bufnr) end) @@ -997,16 +1010,32 @@ function lsp.buf_request(bufnr, method, params, callback) callback = { callback, 'f', true }; } local client_request_ids = {} - for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) - local request_success, request_id = client.request(method, params, callback, resolved_bufnr) - -- This could only fail if the client shut down in the time since we looked - -- it up and we did the request, which should be rare. - if request_success then - client_request_ids[client_id] = request_id + local method_supported = false + for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) + if client.supports_method(method) then + method_supported = true + local request_success, request_id = client.request(method, params, callback, resolved_bufnr) + + -- This could only fail if the client shut down in the time since we looked + -- it up and we did the request, which should be rare. + if request_success then + client_request_ids[client_id] = request_id + end end end) + -- if no clients support the given method, call the callback with the proper + -- error message. + if not method_supported then + local unsupported_err = lsp._unsupported_method(method) + local cb = callback or lsp.callbacks['method'] + if cb then + cb(unsupported_err, method, bufnr) + end + return + end + local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do local client = active_clients[client_id] diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 4e926381e0..2773f59b45 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -703,6 +703,10 @@ function protocol.make_client_capabilities() }; hierarchicalDocumentSymbolSupport = true; }; + rename = { + dynamicRegistration = false; + prepareSupport = true; + }; }; workspace = { symbol = { @@ -914,6 +918,7 @@ function protocol.resolve_capabilities(server_capabilities) return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync)) end end + general_properties.completion = server_capabilities.completionProvider ~= nil general_properties.hover = server_capabilities.hoverProvider or false general_properties.goto_definition = server_capabilities.definitionProvider or false general_properties.find_references = server_capabilities.referencesProvider or false @@ -923,6 +928,15 @@ function protocol.resolve_capabilities(server_capabilities) general_properties.document_formatting = server_capabilities.documentFormattingProvider or false general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false general_properties.call_hierarchy = server_capabilities.callHierarchyProvider or false + general_properties.execute_command = server_capabilities.executeCommandProvider ~= nil + + if server_capabilities.renameProvider == nil then + general_properties.rename = false + elseif type(server_capabilities.renameProvider) == 'boolean' then + general_properties.rename = server_capabilities.renameProvider + else + general_properties.rename = true + end if server_capabilities.codeActionProvider == nil then general_properties.code_action = false -- cgit From 1f0f92f8ec0ca94c47707cfebc9eeff561715368 Mon Sep 17 00:00:00 2001 From: francisco souza Date: Sun, 25 Oct 2020 08:17:34 -0400 Subject: lsp: fix fallback for callback in method_unsupported Missed this #12764. My bad :(( --- runtime/lua/vim/lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index fad213212a..1a0015e2db 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1029,7 +1029,7 @@ function lsp.buf_request(bufnr, method, params, callback) -- error message. if not method_supported then local unsupported_err = lsp._unsupported_method(method) - local cb = callback or lsp.callbacks['method'] + local cb = callback or lsp.callbacks[method] if cb then cb(unsupported_err, method, bufnr) end -- cgit From 7fef16e1d6c106af57039c0620d77db917b12078 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sun, 25 Oct 2020 19:27:11 +0100 Subject: lsp: Store diagnostics for unloaded buffers (#13102) To avoid loading buffers https://github.com/neovim/neovim/pull/12440 changed the logic to not process diagnostics for unloaded buffers. This is problematic for language servers where compile errors or build errors are reported via diagnostics. These errors may prevent the language server from providing all functions and it is difficult for users to debug it without having access to the errors. For example, with eclipse.jdt.ls there may be a problem with gradle (the build tool for java), it results in a diagnostics like this: org.gradle.toolingapi/build.gradle|1 col 1| Could not run build action using Gradle distribution 'https://services.gradle.org/distributions/gradle-4.8.1-bin.zip'. This would be invisible to users unless the user happens to open the right file. In this case the user would actually never see the error, because the language server isn't attached to the build configuration files. This changes the behaviour to at least store the diagnostics. The other operations which are more expensive are still skipped. --- runtime/lua/vim/lsp/callbacks.lua | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 4e7a8333a0..3270d1d2a9 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -82,18 +82,6 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) return end - -- Unloaded buffers should not handle diagnostics. - -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen. - -- This should trigger another publish of the diagnostics. - -- - -- In particular, this stops a ton of spam when first starting a server for current - -- unloaded buffers. - if not api.nvim_buf_is_loaded(bufnr) then - return - end - - util.buf_clear_diagnostics(bufnr) - -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic -- The diagnostic's severity. Can be omitted. If omitted it is up to the -- client to interpret diagnostics as error, warning, info or hint. @@ -104,7 +92,23 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) end end + util.buf_clear_diagnostics(bufnr) + + -- Always save the diagnostics, even if the buf is not loaded. + -- Language servers may report compile or build errors via diagnostics + -- Users should be able to find these, even if they're in files which + -- are not loaded. util.buf_diagnostics_save_positions(bufnr, result.diagnostics) + + -- Unloaded buffers should not handle diagnostics. + -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen. + -- This should trigger another publish of the diagnostics. + -- + -- In particular, this stops a ton of spam when first starting a server for current + -- unloaded buffers. + if not api.nvim_buf_is_loaded(bufnr) then + return + end util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) util.buf_diagnostics_signs(bufnr, result.diagnostics) -- cgit From fd7aa6768a6c341edc1fe768c13899a3d92d8b1d Mon Sep 17 00:00:00 2001 From: Michael Lingelbach Date: Tue, 27 Oct 2020 19:39:24 -0700 Subject: lsp: Fix case where active_signature == vim.NIL (#13114) --- runtime/lua/vim/lsp/util.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index b5f171a985..775932c7fd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -492,7 +492,10 @@ function M.convert_signature_help_to_markdown_lines(signature_help) --=== 0`. Whenever possible implementors should make an active decision about --the active signature and shouldn't rely on a default value. local contents = {} - local active_signature = signature_help.activeSignature or 0 + local active_signature = signature_help.activeSignature + if active_signature == vim.NIL or active_signature == nil then + active_signature = 0 + end -- If the activeSignature is not inside the valid range, then clip it. if active_signature >= #signature_help.signatures then active_signature = 0 -- cgit From e27af090526a6d7fa8f78c585fdc2f7263d5af39 Mon Sep 17 00:00:00 2001 From: Steven Sojka Date: Wed, 28 Oct 2020 07:34:11 -0500 Subject: fix(treesitter): account for no main query file --- runtime/lua/vim/treesitter/query.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 2903c5905c..cc7dc2656d 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -25,7 +25,7 @@ local function filter_files(file_list) end end - return { main, unpack(after) } + return main and { main, unpack(after) } or after end local function runtime_query_path(lang, query_name) -- cgit From 98024853f4755f51e82526b45484bae0ec6042ba Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Fri, 30 Oct 2020 10:58:12 -0400 Subject: lsp: Remove snippet lies (#13183) We don't actually support snippets in core in the way that users would truly expect. So, by default, we will not say that builtin-lsp has `snippetSupport`. To re-enable, users can do the following: First, get a capabilities dictionary with `local capabilities = vim.lsp.protocol.make_client_capabilities()` Then override `capabilities.textDocument.completion.completionItem.snippetSupport = true` and then pass those capabilties to the setup function. ``` nvim_lsp.server_name.setup { ..., capabilities = capabilities, ..., } ``` See https://github.com/neovim/neovim/issues/12795 --- runtime/lua/vim/lsp/protocol.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 2773f59b45..36c9822e23 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -639,8 +639,11 @@ function protocol.make_client_capabilities() completion = { dynamicRegistration = false; completionItem = { + -- Until we can actually expand snippet, move cursor and allow for true snippet experience, + -- this should be disabled out of the box. + -- However, users can turn this back on if they have a snippet plugin. + snippetSupport = false; - snippetSupport = true; commitCharactersSupport = false; preselectSupport = false; deprecatedSupport = false; -- cgit From 03c478ae53c71d0693f1d72b0da9706569cb8fba Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Fri, 30 Oct 2020 10:51:41 +0100 Subject: treesitter: add node:id() --- runtime/lua/vim/treesitter/highlighter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index decde08019..6714bb6354 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -157,7 +157,7 @@ local function on_line_impl(self, buf, line) a.nvim_buf_set_extmark(buf, ns, start_row, start_col, { end_line = end_row, end_col = end_col, hl_group = hl, - ephemeral = true + ephemeral = true, }) end if start_row > line then -- cgit From 720d442d19de4908e22ecf18223358bb7bbb0753 Mon Sep 17 00:00:00 2001 From: Jesse Date: Sun, 1 Nov 2020 18:11:32 +0100 Subject: lsp: complete support for `CodeActionKind`s to capabilities (#13180) We support applying all kinds in the spec equivalently and some servers (including dartls) won't send code actions if support for the relevant kinds is not explicitly stated in the client capabilities. Therefore, this PR makes that support explicit. Also, as we support all CodeActionKinds, we should also mark the server as supporting code actions when it specifies code action kinds. This is also done in this PR. --- runtime/lua/vim/lsp/protocol.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 36c9822e23..70862320c5 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -632,7 +632,7 @@ function protocol.make_client_capabilities() codeActionLiteralSupport = { codeActionKind = { - valueSet = {}; + valueSet = vim.tbl_values(protocol.CodeActionKind); }; }; }; @@ -943,11 +943,9 @@ function protocol.resolve_capabilities(server_capabilities) if server_capabilities.codeActionProvider == nil then general_properties.code_action = false - elseif type(server_capabilities.codeActionProvider) == 'boolean' then + elseif type(server_capabilities.codeActionProvider) == 'boolean' + or type(server_capabilities.codeActionProvider) == 'table' then general_properties.code_action = server_capabilities.codeActionProvider - elseif type(server_capabilities.codeActionProvider) == 'table' then - -- TODO(ashkan) support CodeActionKind - general_properties.code_action = false else error("The server sent invalid codeActionProvider") end -- cgit From dc14b1468afeb53c9ca0a734a2035cc54b5e1d94 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Mon, 2 Nov 2020 08:50:44 -0500 Subject: lsp: remove vim.NIL from processing (#13174) * lsp: remove vim.NIL from processing * lsp: remove instances of vim.NIL --- runtime/lua/vim/lsp/rpc.lua | 19 +++++++++++++++++-- runtime/lua/vim/lsp/util.lua | 22 +++++++++++----------- 2 files changed, 28 insertions(+), 13 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 749a51fecc..17c411f952 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -42,13 +42,28 @@ local function is_dir(filename) end local NIL = vim.NIL + +--@private +local recursive_convert_NIL +recursive_convert_NIL = function(v, tbl_processed) + if v == NIL then + return nil + elseif not tbl_processed[v] and type(v) == 'table' then + tbl_processed[v] = true + return vim.tbl_map(function(x) + return recursive_convert_NIL(x, tbl_processed) + end, v) + end + + return v +end + --@private --- Returns its argument, but converts `vim.NIL` to Lua `nil`. --@param v (any) Argument --@returns (any) local function convert_NIL(v) - if v == NIL then return nil end - return v + return recursive_convert_NIL(v, {}) end --@private diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 775932c7fd..9ed19b938d 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -214,13 +214,16 @@ end function M.apply_text_document_edit(text_document_edit) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) - if text_document.version then - -- `VersionedTextDocumentIdentifier`s version may be null https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier - if text_document.version ~= vim.NIL and M.buf_versions[bufnr] ~= nil and M.buf_versions[bufnr] > text_document.version then - print("Buffer ", text_document.uri, " newer than edits.") - return - end + + -- `VersionedTextDocumentIdentifier`s version may be null + -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier + if text_document.version + and M.buf_versions[bufnr] + and M.buf_versions[bufnr] > text_document.version then + print("Buffer ", text_document.uri, " newer than edits.") + return end + M.apply_text_edits(text_document_edit.edits, bufnr) end @@ -492,10 +495,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help) --=== 0`. Whenever possible implementors should make an active decision about --the active signature and shouldn't rely on a default value. local contents = {} - local active_signature = signature_help.activeSignature - if active_signature == vim.NIL or active_signature == nil then - active_signature = 0 - end + local active_signature = signature_help.activeSignature or 0 -- If the activeSignature is not inside the valid range, then clip it. if active_signature >= #signature_help.signatures then active_signature = 0 @@ -535,7 +535,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help) } --]=] -- TODO highlight parameter - if parameter.documentation and parameter.documentation ~= vim.NIL then + if parameter.documentation then M.convert_input_to_markdown_lines(parameter.documentation, contents) end end -- cgit