diff options
-rw-r--r-- | runtime/doc/api.txt | 53 | ||||
-rw-r--r-- | runtime/doc/lsp-extension.txt | 129 | ||||
-rw-r--r-- | runtime/doc/lsp.txt | 1350 | ||||
-rw-r--r-- | runtime/doc/lua.txt | 1 | ||||
-rw-r--r-- | runtime/lua/vim/F.lua | 24 | ||||
-rw-r--r-- | runtime/lua/vim/highlight.lua | 16 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 216 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 15 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/callbacks.lua | 345 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 1195 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 310 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 25 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 68 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 402 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 4 | ||||
-rwxr-xr-x | scripts/gen_vimdoc.py | 14 | ||||
-rw-r--r-- | scripts/lua2dox.lua | 37 | ||||
-rw-r--r-- | src/nvim/lua/vim.lua | 3 | ||||
-rw-r--r-- | src/nvim/syntax.c | 5 | ||||
-rw-r--r-- | test/functional/plugin/lsp/diagnostic_spec.lua | 767 | ||||
-rw-r--r-- | test/functional/plugin/lsp/handler_spec.lua | 29 | ||||
-rw-r--r-- | test/functional/plugin/lsp_spec.lua | 95 |
22 files changed, 3744 insertions, 1359 deletions
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 0c726ddd86..58633455c3 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -475,6 +475,9 @@ created for extmark changes. ============================================================================== Global Functions *api-global* +nvim__get_hl_defs({ns_id}) *nvim__get_hl_defs()* + TODO: Documentation + nvim__get_lib_dir() *nvim__get_lib_dir()* TODO: Documentation @@ -952,6 +955,9 @@ nvim_get_runtime_file({name}, {all}) *nvim_get_runtime_file()* It is not an error to not find any files. An empty array is returned then. + Attributes: ~ + {fast} + Parameters: ~ {name} pattern of files to search for {all} whether to return all matches or only the first @@ -987,6 +993,7 @@ nvim_input({keys}) *nvim_input()* Note: |keycodes| like <CR> are translated, so "<" is special. To input a literal "<", send <LT>. + Note: For mouse events use |nvim_input_mouse()|. The pseudokey form "<LeftMouse><col,row>" is deprecated since @@ -1378,8 +1385,7 @@ nvim_select_popupmenu_item({item}, {insert}, {finish}, {opts}) {opts} Optional parameters. Reserved for future use. *nvim_set_client_info()* -nvim_set_client_info({name}, {version}, {type}, {methods}, - {attributes}) +nvim_set_client_info({name}, {version}, {type}, {methods}, {attributes}) Self-identifies the client. The client/plugin/application should call this after @@ -1491,7 +1497,7 @@ nvim_set_decoration_provider({ns_id}, {opts}) disable the provider until the next redraw. Similarily, return `false` in `on_win` will skip the `on_lines` calls for that window (but any extmarks set in `on_win` will still be used). - A plugin managing multiple sources of decorations should + A plugin managing multiple sources of decoration should ideally only set one provider, and merge the sources internally. You can use multiple `ns_id` for the extmarks set/modified inside the callback anyway. @@ -1519,6 +1525,33 @@ nvim_set_decoration_provider({ns_id}, {opts}) • on_end: called at the end of a redraw cycle ["end", tick] +nvim_set_hl({ns_id}, {name}, {val}) *nvim_set_hl()* + Set a highlight group. + + TODO: ns_id = 0, should modify :highlight namespace TODO val + should take update vs reset flag + + Parameters: ~ + {ns_id} number of namespace for this highlight + {name} highlight group name, like ErrorMsg + {val} highlight definiton map, like + |nvim_get_hl_by_name|. + +nvim_set_hl_ns({ns_id}) *nvim_set_hl_ns()* + Set active namespace for highlights. + + NB: this function can be called from async contexts, but the + semantics are not yet well-defined. To start with + |nvim_set_decoration_provider| on_win and on_line callbacks + are explicitly allowed to change the namespace during a redraw + cycle. + + Attributes: ~ + {fast} + + Parameters: ~ + {ns_id} the namespace to activate + nvim_set_keymap({mode}, {lhs}, {rhs}, {opts}) *nvim_set_keymap()* Sets a global |mapping| for the given mode. @@ -1618,8 +1651,8 @@ nvim__buf_stats({buffer}) *nvim__buf_stats()* TODO: Documentation *nvim_buf_add_highlight()* -nvim_buf_add_highlight({buffer}, {src_id}, {hl_group}, {line}, - {col_start}, {col_end}) +nvim_buf_add_highlight({buffer}, {src_id}, {hl_group}, {line}, {col_start}, + {col_end}) Adds a highlight to buffer. Useful for plugins that dynamically generate highlights to a @@ -2067,8 +2100,7 @@ nvim_buf_set_keymap({buffer}, {mode}, {lhs}, {rhs}, {opts}) |nvim_set_keymap()| *nvim_buf_set_lines()* -nvim_buf_set_lines({buffer}, {start}, {end}, {strict_indexing}, - {replacement}) +nvim_buf_set_lines({buffer}, {start}, {end}, {strict_indexing}, {replacement}) Sets (replaces) a line-range in the buffer. Indexing is zero-based, end-exclusive. Negative indices are @@ -2116,8 +2148,7 @@ nvim_buf_set_var({buffer}, {name}, {value}) *nvim_buf_set_var()* {value} Variable value *nvim_buf_set_virtual_text()* -nvim_buf_set_virtual_text({buffer}, {src_id}, {line}, {chunks}, - {opts}) +nvim_buf_set_virtual_text({buffer}, {src_id}, {line}, {chunks}, {opts}) Set the virtual text (annotation) for a buffer line. By default (and currently the only option) the text will be @@ -2449,8 +2480,8 @@ nvim_ui_pum_set_bounds({width}, {height}, {row}, {col}) Note that this method is not to be confused with |nvim_ui_pum_set_height()|, which sets the number of visible items in the popup menu, while this function sets the bounding - box of the popup menu, including visual decorations such as - boarders and sliders. Floats need not use the same font size, + box of the popup menu, including visual elements such as + borders and sliders. Floats need not use the same font size, nor be anchored to exact grid corners, so one can set floating-point numbers to the popup menu geometry. diff --git a/runtime/doc/lsp-extension.txt b/runtime/doc/lsp-extension.txt new file mode 100644 index 0000000000..d13303ada6 --- /dev/null +++ b/runtime/doc/lsp-extension.txt @@ -0,0 +1,129 @@ +*lsp-extension.txt* LSP Extension + + NVIM REFERENCE MANUAL + + +The `vim.lsp` Lua module is a framework for building LSP plugins. + + 1. Start with |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()|. + 2. Peek at the API: > + :lua print(vim.inspect(vim.lsp)) +< 3. See |lsp-extension-example| for a full example. + +================================================================================ +LSP EXAMPLE *lsp-extension-example* + +This example is for plugin authors or users who want a lot of control. If you +are just getting started see |lsp-quickstart|. + +For more advanced configurations where just filtering by filetype isn't +sufficient, you can use the `vim.lsp.start_client()` and +`vim.lsp.buf_attach_client()` commands to easily customize the configuration +however you please. For example, if you want to do your own filtering, or +start a new LSP client based on the root directory for working with multiple +projects in a single session. To illustrate, the following is a fully working +Lua example. + +The example will: +1. Check for each new buffer whether or not we want to start an LSP client. +2. Try to find a root directory by ascending from the buffer's path. +3. Create a new LSP for that root directory if one doesn't exist. +4. Attach the buffer to the client for that root directory. + +> + -- Some path manipulation utilities + local function is_dir(filename) + local stat = vim.loop.fs_stat(filename) + return stat and stat.type == 'directory' or false + end + + local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/" + -- Assumes filepath is a file. + local function dirname(filepath) + local is_changed = false + local result = filepath:gsub(path_sep.."([^"..path_sep.."]+)$", function() + is_changed = true + return "" + end) + return result, is_changed + end + + local function path_join(...) + return table.concat(vim.tbl_flatten {...}, path_sep) + end + + -- Ascend the buffer's path until we find the rootdir. + -- is_root_path is a function which returns bool + local function buffer_find_root_dir(bufnr, is_root_path) + local bufname = vim.api.nvim_buf_get_name(bufnr) + if vim.fn.filereadable(bufname) == 0 then + return nil + end + local dir = bufname + -- Just in case our algo is buggy, don't infinite loop. + for _ = 1, 100 do + local did_change + dir, did_change = dirname(dir) + if is_root_path(dir, bufname) then + return dir, bufname + end + -- If we can't ascend further, then stop looking. + if not did_change then + return nil + end + end + end + + -- A table to store our root_dir to client_id lookup. We want one LSP per + -- root directory, and this is how we assert that. + local javascript_lsps = {} + -- Which filetypes we want to consider. + local javascript_filetypes = { + ["javascript.jsx"] = true; + ["javascript"] = true; + ["typescript"] = true; + ["typescript.jsx"] = true; + } + + -- Create a template configuration for a server to start, minus the root_dir + -- which we will specify later. + local javascript_lsp_config = { + name = "javascript"; + cmd = { path_join(os.getenv("JAVASCRIPT_LANGUAGE_SERVER_DIRECTORY"), "lib", "language-server-stdio.js") }; + } + + -- This needs to be global so that we can call it from the autocmd. + function check_start_javascript_lsp() + local bufnr = vim.api.nvim_get_current_buf() + -- Filter which files we are considering. + if not javascript_filetypes[vim.api.nvim_buf_get_option(bufnr, 'filetype')] then + return + end + -- Try to find our root directory. We will define this as a directory which contains + -- node_modules. Another choice would be to check for `package.json`, or for `.git`. + local root_dir = buffer_find_root_dir(bufnr, function(dir) + return is_dir(path_join(dir, 'node_modules')) + -- return vim.fn.filereadable(path_join(dir, 'package.json')) == 1 + -- return is_dir(path_join(dir, '.git')) + end) + -- We couldn't find a root directory, so ignore this file. + if not root_dir then return end + + -- Check if we have a client already or start and store it. + local client_id = javascript_lsps[root_dir] + if not client_id then + local new_config = vim.tbl_extend("error", javascript_lsp_config, { + root_dir = root_dir; + }) + client_id = vim.lsp.start_client(new_config) + javascript_lsps[root_dir] = client_id + end + -- Finally, attach to the buffer to track changes. This will do nothing if we + -- are already attached. + vim.lsp.buf_attach_client(bufnr, client_id) + end + + vim.api.nvim_command [[autocmd BufReadPost * lua check_start_javascript_lsp()]] +< + + vim:tw=78:ts=8:ft=help:norl: diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 33d65406a1..ca6fc46e7b 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -9,6 +9,7 @@ LSP client/framework *lsp* *LSP* Nvim supports the Language Server Protocol (LSP), which means it acts as a client to LSP servers and includes a Lua framework `vim.lsp` for building enhanced LSP tools. + https://microsoft.github.io/language-server-protocol/ LSP facilitates features like go-to-definition, find-references, hover, @@ -20,7 +21,7 @@ analysis (unlike |ctags|). ============================================================================== QUICKSTART *lsp-quickstart* -Nvim provides a LSP client, but the servers are provided by third parties. +Nvim provides an LSP client, but the servers are provided by third parties. Follow these steps to get LSP features: 1. Install the nvim-lspconfig plugin. It provides common configuration for @@ -29,44 +30,62 @@ Follow these steps to get LSP features: 2. Install a language server. Try ":LspInstall <tab>" or use your system package manager to install the relevant language server: https://microsoft.github.io/language-server-protocol/implementors/servers/ - 3. Add `nvim_lsp.xx.setup{…}` to your vimrc, where "xx" is the name of the - relevant config. See the nvim-lspconfig README for details. - -To check LSP clients attached to the current buffer: > + 3. Add `lua require('nvim_lsp').xx.setup{…}` to your init.vim, where "xx" is + the name of the relevant config. See the nvim-lspconfig README for details. + NOTE: Make sure to restart nvim after installing and configuring. + 4. Check that an LSP client has attached to the current buffer: > - :lua print(vim.inspect(vim.lsp.buf_get_clients())) + :lua print(vim.inspect(vim.lsp.buf_get_clients())) < *lsp-config* Inline diagnostics are enabled automatically, e.g. syntax errors will be -annotated in the buffer. But you probably want to use other features like -go-to-definition, hover, etc. Full list of features in |vim.lsp.buf|. - -Example config: > - - nnoremap <silent> <c-]> <cmd>lua vim.lsp.buf.definition()<CR> - nnoremap <silent> K <cmd>lua vim.lsp.buf.hover()<CR> - nnoremap <silent> gD <cmd>lua vim.lsp.buf.implementation()<CR> - nnoremap <silent> <c-k> <cmd>lua vim.lsp.buf.signature_help()<CR> - nnoremap <silent> 1gD <cmd>lua vim.lsp.buf.type_definition()<CR> - nnoremap <silent> gr <cmd>lua vim.lsp.buf.references()<CR> - nnoremap <silent> g0 <cmd>lua vim.lsp.buf.document_symbol()<CR> - nnoremap <silent> gW <cmd>lua vim.lsp.buf.workspace_symbol()<CR> - nnoremap <silent> gd <cmd>lua vim.lsp.buf.declaration()<CR> - -Note: Language servers may have limited support for these features. - -Nvim provides the |vim.lsp.omnifunc| 'omnifunc' handler which allows -|i_CTRL-X_CTRL-O| to consume LSP completion. Example config (note the use of -|v:lua| to call Lua from Vimscript): > - - " Use LSP omni-completion in Python files. - autocmd Filetype python setlocal omnifunc=v:lua.vim.lsp.omnifunc +annotated in the buffer. But you probably also want to use other features +like go-to-definition, hover, etc. + +While Nvim does not provide an "auto-completion" framework by default, it is +still possible to get completions from the LSP server. To incorporate these +completions, it is recommended to use |vim.lsp.omnifunc|, which is an 'omnifunc' +handler. When 'omnifunc' is set to `v:lua.vim.lsp.omnifunc`, |i_CTRL-X_CTRL-O| +will provide completions from the language server. + +Example config (in init.vim): > + + lua << EOF + local custom_lsp_attach = function(client) + -- See `:help nvim_buf_set_keymap()` for more information + vim.api.nvim_buf_set_keymap(0, 'n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>', {noremap = true}) + vim.api.nvim_buf_set_keymap(0, 'n', '<c-]>', '<cmd>lua vim.lsp.buf.definition()<CR>', {noremap = true}) + -- ... and other keymappings for LSP + + -- Use LSP as the handler for omnifunc. + -- See `:help omnifunc` and `:help ins-completion` for more information. + vim.api.nvim_buf_set_option(0, 'omnifunc', 'v:lua.vim.lsp.omnifunc') + + -- For plugins with an `on_attach` callback, call them here. For example: + -- require('completion').on_attach(client) + end -If a function has a `*_sync` variant, it's primarily intended for being run -automatically on file save. E.g. code formatting: > + -- An example of configuring for `sumneko_lua`, + -- a language server for Lua. + -- First, you must run `:LspInstall sumneko_lua` for this to work. + require('nvim_lsp').sumneko_lua.setup({ + -- An example of settings for an LSP server. + -- For more options, see nvim-lspconfig + settings = { + Lua = { + diagnostics = { + enable = true, + globals = { "vim" }, + }, + } + }, + + on_attach = custom_lsp_attach + }) + EOF +< - " Auto-format *.rs files prior to saving them - autocmd BufWritePre *.rs lua vim.lsp.buf.formatting_sync(nil, 1000) +Full list of features provided by default can be found in |lsp-buf|. ================================================================================ FAQ *lsp-faq* @@ -74,28 +93,52 @@ FAQ *lsp-faq* - Q: How to force-reload LSP? A: Stop all clients, then reload the buffer. > - :lua vim.lsp.stop_client(vim.lsp.get_active_clients()) - :edit + :lua vim.lsp.stop_client(vim.lsp.get_active_clients()) + :edit - Q: Why isn't completion working? A: In the buffer where you want to use LSP, check that 'omnifunc' is set to - "v:lua.vim.lsp.omnifunc": > + "v:lua.vim.lsp.omnifunc": > - :verbose set omnifunc? + :verbose set omnifunc? -< Some other plugin may be overriding the option. To avoid that, you could - set the option in an |after-directory| ftplugin, e.g. - "after/ftplugin/python.vim". +< Some other plugin may be overriding the option. To avoid that, you could + set the option in an |after-directory| ftplugin, e.g. + "after/ftplugin/python.vim". -================================================================================ -LSP API *lsp-api* +- Q: How do I run a request synchronously (e.g. for formatting on file save)? + A: Use the `_sync` variant of the function provided by |lsp-buf|, if it + exists. -The `vim.lsp` Lua module is a framework for building LSP plugins. + E.g. code formatting: > - 1. Start with |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()|. - 2. Peek at the API: > - :lua print(vim.inspect(vim.lsp)) -< 3. See |lsp-extension-example| for a full example. + " Auto-format *.rs (rust) files prior to saving them + autocmd BufWritePre *.rs lua vim.lsp.buf.formatting_sync(nil, 1000) + +< + *vim.lsp.callbacks* +- Q: What happened to `vim.lsp.callbacks`? + A: After better defining the interface of |lsp-hander|s, we thought it best + to remove the generic usage of `callbacks` and transform to `handlers`. + Due to this, `vim.lsp.callbacks` was renamed to |vim.lsp.handlers|. + + *lsp-vs-treesitter* +- Q: How do LSP and Treesitter compare? + A: LSP requires a client and language server. The language server uses + semantic analysis to understand code at a project level. This provides + language servers with the ability to rename across files, find + definitions in external libraries and more. + + Treesitter is a language parsing library that provides excellent tools + for incrementally parsing text and handling errors. This makes it a great + fit for editors to understand the contents of the current file for things + like syntax highlighting, simple goto-definitions, scope analysis and + more. + + LSP and Treesitter are both great tools for editing and inspecting code. + +================================================================================ +LSP API *lsp-api* LSP core API is described at |lsp-core|. Those are the core functions for creating and managing clients. @@ -103,58 +146,209 @@ creating and managing clients. The `vim.lsp.buf_…` functions perform operations for all LSP clients attached to the given buffer. |lsp-buf| -LSP request/response handlers are implemented as Lua callbacks. -|lsp-callbacks| The `vim.lsp.callbacks` table defines default callbacks used +LSP request/response handlers are implemented as Lua functions (see +|lsp-handler|). The |vim.lsp.handlers| table defines default handlers used when creating a new client. Keys are LSP method names: > - :lua print(vim.inspect(vim.tbl_keys(vim.lsp.callbacks))) - -These LSP requests/notifications are defined by default: - - textDocument/publishDiagnostics - window/logMessage - window/showMessage + :lua print(vim.inspect(vim.tbl_keys(vim.lsp.handlers))) +< + *lsp-method* + +Methods are the names of requests and notifications as defined by the LSP +specification. These LSP requests/notifications are defined by default: + + callHierarchy/incomingCalls + callHierarchy/outgoingCalls + textDocument/codeAction + textDocument/completion + textDocument/declaration* + textDocument/definition + textDocument/documentHighlight + textDocument/documentSymbol + textDocument/formatting + textDocument/hover + textDocument/implementation* + textDocument/publishDiagnostics + textDocument/rangeFormatting + textDocument/references + textDocument/rename + textDocument/signatureHelp + textDocument/typeDefinition* + window/logMessage + window/showMessage + workspace/applyEdit + workspace/symbol + +* NOTE: These are sometimes not implemented by servers. + + *lsp-handler* + +lsp-handlers are functions with special signatures that are designed to handle +responses and notifications from LSP servers. + +For |lsp-request|, each |lsp-handler| has this signature: > + + function(err, method, result, client_id, bufnr, config) +< + Parameters: ~ + {err} (table|nil) + When the language server is unable to complete a + request, a table with information about the error + is sent. Otherwise, it is `nil`. See |lsp-response|. + {method} (string) + The |lsp-method| name. + {result} (Result | Params | nil) + When the language server is able to succesfully + complete a request, this contains the `result` key + of the response. See |lsp-response|. + {client_id} (number) + The ID of the |vim.lsp.client|. + {bufnr} (Buffer) + Buffer handle, or 0 for current. + {config} (table) + Configuration for the handler. + + Each handler can define it's own configuration + table that allows users to customize the behavior + of a particular handler. + + To configure a particular |lsp-handler|, see: + |lsp-handler-configuration| + + Returns: ~ + The |lsp-handler| can respond by returning two values: `result, err` + Where `err` must be shaped like an RPC error: + `{ code, message, data? }` + + You can use |vim.lsp.rpc_response_error()| to create this object. + +For |lsp-notification|, each |lsp-handler| has this signature: > + + function(err, method, params, client_id, bufnr, config) +< + Parameters: ~ + {err} (nil) + This is always `nil`. + See |lsp-notification| + {method} (string) + The |lsp-method| name. + {params} (Params) + This contains the `params` key of the notification. + See |lsp-notification| + {client_id} (number) + The ID of the |vim.lsp.client| + {bufnr} (nil) + `nil`, as the server doesn't have an associated buffer. + {config} (table) + Configuration for the handler. + + Each handler can define it's own configuration + table that allows users to customize the behavior + of a particular handler. + + For an example, see: + |vim.lsp.diagnostics.on_publish_diagnostics()| + + To configure a particular |lsp-handler|, see: + |lsp-handler-configuration| + + Returns: ~ + The |lsp-handler|'s return value will be ignored. + + *lsp-handler-configuration* + +To configure the behavior of a builtin |lsp-handler|, the conenvience method +|vim.lsp.with()| is provided for users. + + To configure the behavior of |vim.lsp.diagnostic.on_publish_diagnostics()|, + consider the following example, where a new |lsp-handler| is created using + |vim.lsp.with()| that no longer generates signs for the diagnostics: > + + vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( + vim.lsp.diagnostic.on_publish_diagnostics, { + -- Disable signs + signs = false, + } + ) +< + To enable signs, use |vim.lsp.with()| again to create and assign a new + |lsp-handler| to |vim.lsp.handlers| for the associated method: > + + vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( + vim.lsp.diagnostic.on_publish_diagnostics, { + -- Enable signs + signs = true, + } + ) +< + To configure a handler on a per-server basis, you can use the {handlers} key + for |vim.lsp.start_client()| > + + vim.lsp.start_client { + ..., -- Other configuration omitted. + handlers = { + ["textDocument/publishDiagnostics"] = vim.lsp.with( + vim.lsp.diagnostic.on_publish_diagnostics, { + -- Disable virtual_text + virtual_text = false, + } + }, + } +< + or if using 'nvim-lspconfig', you can use the {handlers} key of `setup()`: > + + nvim_lsp.rust_analyzer.setup { + handlers = { + ["textDocument/publishDiagnostics"] = vim.lsp.with( + vim.lsp.diagnostic.on_publish_diagnostics, { + -- Disable virtual_text + virtual_text = false + } + ), + } + } +< + *lsp-handler-resolution* +Handlers can be set by: -You can check these via `vim.tbl_keys(vim.lsp.callbacks)`. +- Setting a field in |vim.lsp.handlers|. *vim.lsp.handlers* + |vim.lsp.handlers| is a global table that contains the default mapping of + |lsp-method| names to |lsp-handlers|. -These will be used preferentially in `vim.lsp.buf_…` methods for handling -requests. They will also be used when responding to server requests and -notifications. + To override the handler for the `"textDocument/definition"` method: > -Use cases: -- Users can modify this to customize to their preferences. -- UI plugins can modify this by assigning to - `vim.lsp.callbacks[method]` so as to provide more specialized - handling, allowing you to leverage the UI capabilities available. UIs should - try to be conscientious of any existing changes the user may have set - already by checking for existing values. + vim.lsp.handlers["textDocument/definition"] = my_custom_default_definition +< +- The {handlers} parameter for |vim.lsp.start_client|. + This will set the |lsp-handler| as the default handler for this server. -Any callbacks passed directly to `request` methods on a server client will -have the highest precedence, followed by the `callbacks`. + For example: > -You can override the default handlers, -- globally: by modifying the `vim.lsp.callbacks` table -- per-client: by passing the {callbacks} table parameter to - |vim.lsp.start_client| + vim.lsp.start_client { + ..., -- Other configuration ommitted. + handlers = { + ["textDocument/definition"] = my_custom_server_definition + }, + } -Each handler has this signature: > +- The {handler} parameter for |vim.lsp.buf_request()|. + This will set the |lsp-handler| ONLY for the current request. - function(err, method, params, client_id) + For example: > -Callbacks are functions which are called in a variety of situations by the -client. Their signature is `function(err, method, params, client_id)` They can -be set by the {callbacks} parameter for |vim.lsp.start_client| or via the -|vim.lsp.callbacks|. + vim.lsp.buf_request( + 0, + "textDocument/definition", + definition_params, + my_request_custom_definition + ) +< +In summary, the |lsp-handler| will be chosen based on the current |lsp-method| +in the following order: -Handlers are called for: -- Notifications from the server (`err` is always `nil`). -- Requests initiated by the server (`err` is always `nil`). - The handler can respond by returning two values: `result, err` - where `err` must be shaped like an RPC error: - `{ code, message, data? }` - You can use |vim.lsp.rpc_response_error()| to create this object. -- Handling requests initiated by the client if the request doesn't explicitly - specify a callback (such as in |vim.lsp.buf_request|). +1. Handler passed to |vim.lsp.buf_request()|, if any. +2. Handler defined in |vim.lsp.start_client()|, if any. +3. Handler defined in |vim.lsp.handlers|, if any. VIM.LSP.PROTOCOL *vim.lsp.protocol* @@ -168,41 +362,21 @@ name: > vim.lsp.protocol.TextDocumentSyncKind.Full == 1 vim.lsp.protocol.TextDocumentSyncKind[1] == "Full" +< + + *lsp-response* +For the format of the response message, see: + https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage + + *lsp-notification* +For the format of the notification message, see: + https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage ================================================================================ LSP HIGHLIGHT *lsp-highlight* - *hl-LspDiagnosticsError* -LspDiagnosticsError used for "Error" diagnostic virtual text - *hl-LspDiagnosticsErrorSign* -LspDiagnosticsErrorSign used for "Error" diagnostic signs in sign - column - *hl-LspDiagnosticsErrorFloating* -LspDiagnosticsErrorFloating used for "Error" diagnostic messages in the - diagnostics float - *hl-LspDiagnosticsWarning* -LspDiagnosticsWarning used for "Warning" diagnostic virtual text - *hl-LspDiagnosticsWarningSign* -LspDiagnosticsWarningSign used for "Warning" diagnostic signs in sign - column - *hl-LspDiagnosticsWarningFloating* -LspDiagnosticsWarningFloating used for "Warning" diagnostic messages in the - diagnostics float - *hl-LspDiagnosticsInformation* -LspDiagnosticsInformation used for "Information" diagnostic virtual text - *hl-LspDiagnosticsInformationSign* -LspDiagnosticsInformationSign used for "Information" signs in sign column - *hl-LspDiagnosticsInformationFloating* -LspDiagnosticsInformationFloating used for "Information" diagnostic messages in - the diagnostics float - *hl-LspDiagnosticsHint* -LspDiagnosticsHint used for "Hint" diagnostic virtual text - *hl-LspDiagnosticsHintSign* -LspDiagnosticsHintSign used for "Hint" diagnostic signs in sign - column - *hl-LspDiagnosticsHintFloating* -LspDiagnosticsHintFloating used for "Hint" diagnostic messages in the - diagnostics float +Reference Highlights: + *hl-LspReferenceText* LspReferenceText used for highlighting "text" references *hl-LspReferenceRead* @@ -211,122 +385,120 @@ LspReferenceRead used for highlighting "read" references LspReferenceWrite used for highlighting "write" references -================================================================================ -LSP EXAMPLE *lsp-extension-example* - -This example is for plugin authors or users who want a lot of control. If you -are just getting started see |lsp-quickstart|. + *lsp-highlight-diagnostics* +All highlights defined for diagnostics begin with `LspDiagnostics` followed by +the type of highlight (e.g., `Sign`, `Underline`, etc.) and then the Severity +of the highlight (e.g. `Error`, `Warning`, etc.) -For more advanced configurations where just filtering by filetype isn't -sufficient, you can use the `vim.lsp.start_client()` and -`vim.lsp.buf_attach_client()` commands to easily customize the configuration -however you please. For example, if you want to do your own filtering, or -start a new LSP client based on the root directory for if you plan to work -with multiple projects in a single session. Below is a fully working Lua -example which can do exactly that. +Sign, underline and virtual text highlights (by default) are linked to their +corresponding LspDiagnosticsDefault highlight. -The example will: -1. Check for each new buffer whether or not we want to start an LSP client. -2. Try to find a root directory by ascending from the buffer's path. -3. Create a new LSP for that root directory if one doesn't exist. -4. Attach the buffer to the client for that root directory. +For example, the default highlighting for |hl-LspDiagnosticsSignError| is +linked to |hl-LspDiagnosticsDefaultError|. To change the default (and +therefore the linked highlights), use the |:highlight| command: > -> - -- Some path manipulation utilities - local function is_dir(filename) - local stat = vim.loop.fs_stat(filename) - return stat and stat.type == 'directory' or false - end - - local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/" - -- Asumes filepath is a file. - local function dirname(filepath) - local is_changed = false - local result = filepath:gsub(path_sep.."([^"..path_sep.."]+)$", function() - is_changed = true - return "" - end) - return result, is_changed - end - - local function path_join(...) - return table.concat(vim.tbl_flatten {...}, path_sep) - end - - -- Ascend the buffer's path until we find the rootdir. - -- is_root_path is a function which returns bool - local function buffer_find_root_dir(bufnr, is_root_path) - local bufname = vim.api.nvim_buf_get_name(bufnr) - if vim.fn.filereadable(bufname) == 0 then - return nil - end - local dir = bufname - -- Just in case our algo is buggy, don't infinite loop. - for _ = 1, 100 do - local did_change - dir, did_change = dirname(dir) - if is_root_path(dir, bufname) then - return dir, bufname - end - -- If we can't ascend further, then stop looking. - if not did_change then - return nil - end - end - end - - -- A table to store our root_dir to client_id lookup. We want one LSP per - -- root directory, and this is how we assert that. - local javascript_lsps = {} - -- Which filetypes we want to consider. - local javascript_filetypes = { - ["javascript.jsx"] = true; - ["javascript"] = true; - ["typescript"] = true; - ["typescript.jsx"] = true; - } - - -- Create a template configuration for a server to start, minus the root_dir - -- which we will specify later. - local javascript_lsp_config = { - name = "javascript"; - cmd = { path_join(os.getenv("JAVASCRIPT_LANGUAGE_SERVER_DIRECTORY"), "lib", "language-server-stdio.js") }; - } - - -- This needs to be global so that we can call it from the autocmd. - function check_start_javascript_lsp() - local bufnr = vim.api.nvim_get_current_buf() - -- Filter which files we are considering. - if not javascript_filetypes[vim.api.nvim_buf_get_option(bufnr, 'filetype')] then - return - end - -- Try to find our root directory. We will define this as a directory which contains - -- node_modules. Another choice would be to check for `package.json`, or for `.git`. - local root_dir = buffer_find_root_dir(bufnr, function(dir) - return is_dir(path_join(dir, 'node_modules')) - -- return vim.fn.filereadable(path_join(dir, 'package.json')) == 1 - -- return is_dir(path_join(dir, '.git')) - end) - -- We couldn't find a root directory, so ignore this file. - if not root_dir then return end - - -- Check if we have a client alredy or start and store it. - local client_id = javascript_lsps[root_dir] - if not client_id then - local new_config = vim.tbl_extend("error", javascript_lsp_config, { - root_dir = root_dir; - }) - client_id = vim.lsp.start_client(new_config) - javascript_lsps[root_dir] = client_id - end - -- Finally, attach to the buffer to track changes. This will do nothing if we - -- are already attached. - vim.lsp.buf_attach_client(bufnr, client_id) - end - - vim.api.nvim_command [[autocmd BufReadPost * lua check_start_javascript_lsp()]] + highlight LspDiagnosticsDefaultError guifg="BrightRed" < + *hl-LspDiagnosticsDefaultError* +LspDiagnosticsDefaultError + Used as the base highlight group. + Other LspDiagnostic highlights link to this by default (except Underline) + + *hl-LspDiagnosticsDefaultWarning* +LspDiagnosticsDefaultWarning + Used as the base highlight group. + Other LspDiagnostic highlights link to this by default (except Underline) + + *hl-LspDiagnosticsDefaultInformation* +LspDiagnosticsDefaultInformation + Used as the base highlight group. + Other LspDiagnostic highlights link to this by default (except Underline) + + *hl-LspDiagnosticsDefaultHint* +LspDiagnosticsDefaultHint + Used as the base highlight group. + Other LspDiagnostic highlights link to this by default (except Underline) + + *hl-LspDiagnosticsVirtualTextError* +LspDiagnosticsVirtualTextError + Used for "Error" diagnostic virtual text. + See |vim.lsp.diagnostic.set_virtual_text()| + + *hl-LspDiagnosticsVirtualTextWarning* +LspDiagnosticsVirtualTextWarning + Used for "Warning" diagnostic virtual text. + See |vim.lsp.diagnostic.set_virtual_text()| + + *hl-LspDiagnosticsVirtualTextInformation* +LspDiagnosticsVirtualTextInformation + Used for "Information" diagnostic virtual text. + See |vim.lsp.diagnostic.set_virtual_text()| + + *hl-LspDiagnosticsVirtualTextHint* +LspDiagnosticsVirtualTextHint + Used for "Hint" diagnostic virtual text. + See |vim.lsp.diagnostic.set_virtual_text()| + + *hl-LspDiagnosticsUnderlineError* +LspDiagnosticsUnderlineError + Used to underline "Error" diagnostics. + See |vim.lsp.diagnostic.set_underline()| + + *hl-LspDiagnosticsUnderlineWarning* +LspDiagnosticsUnderlineWarning + Used to underline "Warning" diagnostics. + See |vim.lsp.diagnostic.set_underline()| + + *hl-LspDiagnosticsUnderlineInformation* +LspDiagnosticsUnderlineInformation + Used to underline "Information" diagnostics. + See |vim.lsp.diagnostic.set_underline()| + + *hl-LspDiagnosticsUnderlineHint* +LspDiagnosticsUnderlineHint + Used to underline "Hint" diagnostics. + See |vim.lsp.diagnostic.set_underline()| + + *hl-LspDiagnosticsFloatingError* +LspDiagnosticsFloatingError + Used to color "Error" diagnostic messages in diagnostics float. + See |vim.lsp.diagnostic.show_line_diagnostics()| + + *hl-LspDiagnosticsFloatingWarning* +LspDiagnosticsFloatingWarning + Used to color "Warning" diagnostic messages in diagnostics float. + See |vim.lsp.diagnostic.show_line_diagnostics()| + + *hl-LspDiagnosticsFloatingInformation* +LspDiagnosticsFloatingInformation + Used to color "Information" diagnostic messages in diagnostics float. + See |vim.lsp.diagnostic.show_line_diagnostics()| + + *hl-LspDiagnosticsFloatingHint* +LspDiagnosticsFloatingHint + Used to color "Hint" diagnostic messages in diagnostics float. + See |vim.lsp.diagnostic.show_line_diagnostics()| + + *hl-LspDiagnosticsSignError* +LspDiagnosticsSignError + Used for "Error" signs in sign column. + See |vim.lsp.diagnostic.set_signs()| + + *hl-LspDiagnosticsSignWarning* +LspDiagnosticsSignWarning + Used for "Warning" signs in sign column. + See |vim.lsp.diagnostic.set_signs()| + + *hl-LspDiagnosticsSignInformation* +LspDiagnosticsSignInformation + Used for "Information" signs in sign column. + See |vim.lsp.diagnostic.set_signs()| + + *hl-LspDiagnosticsSignHint* +LspDiagnosticsSignHint + Used for "Hint" signs in sign column. + See |vim.lsp.diagnostic.set_signs()| ============================================================================== AUTOCOMMANDS *lsp-autocommands* @@ -376,16 +548,16 @@ buf_notify({bufnr}, {method}, {params}) *vim.lsp.buf_notify()* true if any client returns true; false otherwise *vim.lsp.buf_request()* -buf_request({bufnr}, {method}, {params}, {callback}) +buf_request({bufnr}, {method}, {params}, {handler}) Sends an async request for all active clients attached to the buffer. Parameters: ~ - {bufnr} (number) Buffer handle, or 0 for current. - {method} (string) LSP method name - {params} (optional, table) Parameters to send to the - server - {callback} (optional, functionnil) Handler + {bufnr} (number) Buffer handle, or 0 for current. + {method} (string) LSP method name + {params} (optional, table) Parameters to send to the + server + {handler} (optional, function) See |lsp-handler| Return: ~ 2-tuple: @@ -423,17 +595,16 @@ client() *vim.lsp.client* |vim.lsp.get_active_clients()|. • Methods: - • request(method, params, [callback], bufnr) Sends a request + • request(method, params, [handler], bufnr) Sends a request to the server. This is a thin wrapper around {client.rpc.request} with some additional checking. If - {callback} is not specified, it will use - {client.callbacks} to try to find a callback. If one is - not found there, then an error will occur. Returns: - {status}, {[client_id]}. {status} is a boolean indicating - if the notification was successful. If it is `false` , - then it will always be `false` (the client has shutdown). - If {status} is `true` , the function returns {request_id} - as the second result. You can use this with + {handler} is not specified, If one is not found there, + then an error will occur. Returns: {status}, + {[client_id]}. {status} is a boolean indicating if the + notification was successful. If it is `false` , then it + will always be `false` (the client has shutdown). If + {status} is `true` , the function returns {request_id} as + the second result. You can use this with `client.cancel_request(request_id)` to cancel the request. • notify(method, params) Sends a notification to an LSP server. Returns: a boolean to indicate if the notification @@ -462,8 +633,8 @@ client() *vim.lsp.client* communicating with the server. You can modify this in the `config` 's `on_init` method before text is sent to the server. - • {callbacks} (table): The callbacks used by the client as - described in |lsp-callbacks|. + • {handlers} (table): The handlers used by the client as + described in |lsp-handler|. • {config} (table): copy of the table that was passed by the user to |vim.lsp.start_client()|. • {server_capabilities} (table): Response from the server @@ -488,8 +659,8 @@ get_active_clients() *vim.lsp.get_active_clients()* Table of |vim.lsp.client| objects get_client_by_id({client_id}) *vim.lsp.get_client_by_id()* - Gets a client by id, or nil if the id is invalid. - The returned client may not yet be fully initialized. + Gets a client by id, or nil if the id is invalid. The returned + client may not yet be fully initialized. Parameters: ~ {client_id} client id number @@ -573,19 +744,8 @@ start_client({config}) *vim.lsp.start_client()* `{[vim.type_idx]=vim.types.dictionary}` , else it will be encoded as an array. - {callbacks} Map of language server method names to `function(err, method, params, - client_id)` handler. Invoked for: - • Notifications to the server, where - `err` will always be `nil` . - • Requests by the server. For these you - can respond by returning two values: - `result, err` where err must be - shaped like a RPC error, i.e. `{ - code, message, data? }` . Use - |vim.lsp.rpc_response_error()| to - help with this. - • Default callback for client requests - not explicitly specifying a callback. + {handlers} Map of language server method names to + |lsp-handler| {init_options} Values to pass in the initialization request as `initializationOptions` . See `initialize` in the LSP spec. @@ -638,11 +798,9 @@ start_client({config}) *vim.lsp.start_client()* "off" Return: ~ - 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 the client - has been initialized. + 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. stop_client({client_id}, {force}) *vim.lsp.stop_client()* Stops a client(s). @@ -663,26 +821,13 @@ stop_client({client_id}, {force}) *vim.lsp.stop_client()* thereof {force} boolean (optional) shutdown forcefully +with({handler}, {override_config}) *vim.lsp.with()* + Function to manage overriding defaults for LSP handlers. -============================================================================== -Lua module: vim.lsp.protocol *lsp-protocol* - - *vim.lsp.protocol.make_client_capabilities()* -make_client_capabilities() - Gets a new ClientCapabilities object describing the LSP client - capabilities. - - *vim.lsp.protocol.resolve_capabilities()* -resolve_capabilities({server_capabilities}) - `*` to match one or more characters in a path segment `?` to - match on one character in a path segment `**` to match any - number of path segments, including none `{}` to group - conditions (e.g. `**/*.{ts,js}` matches all TypeScript and - JavaScript files) `[]` to declare a range of characters to - match in a path segment (e.g., `example.[0-9]` to match on - `example.0` , `example.1` , …) `[!...]` to negate a range of - characters to match in a path segment (e.g., `example.[!0-9]` - to match on `example.a` , `example.b` , but not `example.0` ) + Parameters: ~ + {handler} (function) See |lsp-handler| + {override_config} (table) Table containing the keys to + override behavior of the {handler} ============================================================================== @@ -718,6 +863,9 @@ completion({context}) *vim.lsp.buf.completion()* declaration() *vim.lsp.buf.declaration()* Jumps to the declaration of the symbol under the cursor. + Note: + Many servers do not implement this method. Generally, see + |vim.lsp.buf.definition()| instead. definition() *vim.lsp.buf.definition()* Jumps to the definition of the symbol under the cursor. @@ -862,221 +1010,380 @@ workspace_symbol({query}) *vim.lsp.buf.workspace_symbol()* ============================================================================== -Lua module: vim.lsp.log *lsp-log* +Lua module: vim.lsp.diagnostic *lsp-diagnostic* -get_filename() *vim.lsp.log.get_filename()* - Returns the log filename. + *vim.lsp.diagnostic.clear()* +clear({bufnr}, {client_id}, {diagnostic_ns}, {sign_ns}) + Clears the currently displayed diagnostics - Return: ~ - (string) log filename + Parameters: ~ + {bufnr} number The buffer number + {client_id} number the client id + {diagnostic_ns} number|nil Associated diagnostic + namespace + {sign_ns} number|nil Associated sign namespace -set_level({level}) *vim.lsp.log.set_level()* - Sets the current log level. +get({bufnr}, {client_id}) *vim.lsp.diagnostic.get()* + Return associated diagnostics for bufnr Parameters: ~ - {level} (string or number) One of `vim.lsp.log.levels` + {bufnr} number + {client_id} number|nil If nil, then return all of the + diagnostics. Else, return just the + diagnostics associated with the client_id. -should_log({level}) *vim.lsp.log.should_log()* - Checks whether the level is sufficient for logging. + *vim.lsp.diagnostic.get_count()* +get_count({bufnr}, {severity}, {client_id}) + Get the counts for a particular severity + + Useful for showing diagnostic counts in statusline. eg: +> + + function! LspStatus() abort + let sl = '' + if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))') + let sl.='%#MyStatuslineLSP#E:' + let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.diagnostic.get_count([[Error]])")}' + let sl.='%#MyStatuslineLSP# W:' + let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.diagnostic.get_count([[Warning]])")}' + else + let sl.='%#MyStatuslineLSPErrors#off' + endif + return sl + endfunction + let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus() +< Parameters: ~ - {level} number log level + {bufnr} number The buffer number + {severity} DiagnosticSeverity + {client_id} number the client id - Return: ~ - (bool) true if would log, false if not + *vim.lsp.diagnostic.get_line_diagnostics()* +get_line_diagnostics({bufnr}, {line_nr}, {opts}, {client_id}) + Get the diagnostics by line + Parameters: ~ + {bufnr} number The buffer number + {line_nr} number The line number + {opts} table|nil Configuration keys + • severity: (DiagnosticSeverity, default nil) + • Only return diagnostics with this + severity. Overrides severity_limit -============================================================================== -Lua module: vim.lsp.rpc *lsp-rpc* + • severity_limit: (DiagnosticSeverity, default nil) + • Limit severity of diagnostics found. E.g. + "Warning" means { "Error", "Warning" } + will be valid. + {client_id} number the client id -format_rpc_error({err}) *vim.lsp.rpc.format_rpc_error()* - Constructs an error message from an LSP error object. + Return: ~ + table Table with map of line number to list of + diagnostics. + +get_next({opts}) *vim.lsp.diagnostic.get_next()* + Get the previous diagnostic closest to the cursor_position Parameters: ~ - {err} (table) The error object + {opts} table See |vim.lsp.diagnostics.goto_next()| Return: ~ - (string) The formatted error message + table Next diagnostic -notify({method}, {params}) *vim.lsp.rpc.notify()* - Sends a notification to the LSP server. +get_next_pos({opts}) *vim.lsp.diagnostic.get_next_pos()* + Return the pos, {row, col}, for the next diagnostic in the + current buffer. Parameters: ~ - {method} (string) The invoked LSP method - {params} (table): Parameters for the invoked LSP method + {opts} table See |vim.lsp.diagnostics.goto_next()| Return: ~ - (bool) `true` if notification could be sent, `false` if - not + table Next diagnostic position -request({method}, {params}, {callback}) *vim.lsp.rpc.request()* - Sends a request to the LSP server and runs {callback} upon - response. +get_prev({opts}) *vim.lsp.diagnostic.get_prev()* + Get the previous diagnostic closest to the cursor_position Parameters: ~ - {method} (string) The invoked LSP method - {params} (table) Parameters for the invoked LSP method - {callback} (function) Callback to invoke + {opts} table See |vim.lsp.diagnostics.goto_next()| Return: ~ - (bool, number) `(true, message_id)` if request could be - sent, `false` if not + table Previous diagnostic - *vim.lsp.rpc.rpc_response_error()* -rpc_response_error({code}, {message}, {data}) - Creates an RPC response object/table. +get_prev_pos({opts}) *vim.lsp.diagnostic.get_prev_pos()* + Return the pos, {row, col}, for the prev diagnostic in the + current buffer. Parameters: ~ - {code} RPC error code defined in - `vim.lsp.protocol.ErrorCodes` - {message} (optional) arbitrary message to send to server - {data} (optional) arbitrary data to send to server + {opts} table See |vim.lsp.diagnostics.goto_next()| - *vim.lsp.rpc.start()* -start({cmd}, {cmd_args}, {handlers}, {extra_spawn_params}) - Starts an LSP server process and create an LSP RPC client - object to interact with it. + Return: ~ + table Previous diagnostic position + + *vim.lsp.diagnostic.get_virtual_text_chunks_for_line()* +get_virtual_text_chunks_for_line({bufnr}, {line}, {line_diags}, {opts}) + Default function to get text chunks to display using `nvim_buf_set_virtual_text` . Parameters: ~ - {cmd} (string) Command to start the LSP - server. - {cmd_args} (table) List of additional string - arguments to pass to {cmd}. - {handlers} (table, optional) Handlers for LSP - message types. Valid handler names - are: - • `"notification"` - • `"server_request"` - • `"on_error"` - • `"on_exit"` - {extra_spawn_params} (table, optional) Additional context - for the LSP server process. May - contain: - • {cwd} (string) Working directory - for the LSP server process - • {env} (table) Additional - environment variables for LSP - server process + {bufnr} number The buffer to display the virtual + text in + {line} number The line number to display the + virtual text on + {line_diags} Diagnostic [] The diagnostics associated with the line + {opts} table See {opts} from + |vim.lsp.diagnostic.set_virtual_text()| Return: ~ - Client RPC object. - Methods: - • `notify()` |vim.lsp.rpc.notify()| - • `request()` |vim.lsp.rpc.request()| + table chunks, as defined by |nvim_buf_set_virtual_text()| + +goto_next({opts}) *vim.lsp.diagnostic.goto_next()* + Move to the next diagnostic + + Parameters: ~ + {opts} table|nil Configuration table. Keys: + • {client_id}: (number) + • If nil, will consider all clients attached to + buffer. + + • {cursor_position}: (Position, default current + position) + • See |nvim_win_get_cursor()| + + • {wrap}: (boolean, default true) + • Whether to loop around file or not. Similar to + 'wrapscan' + + • {severity}: (DiagnosticSeverity) + • Exclusive severity to consider. Overrides + {severity_limit} + + • {severity_limit}: (DiagnosticSeverity) + • Limit severity of diagnostics found. E.g. + "Warning" means { "Error", "Warning" } will be + valid. + + • {enable_popup}: (boolean, default true) + • Call + |vim.lsp.diagnostic.show_line_diagnostics()| + on jump + + • {popup_opts}: (table) + • Table to pass as {opts} parameter to + |vim.lsp.diagnostic.show_line_diagnostics()| + + • {win_id}: (number, default 0) + • Window ID + +goto_prev({opts}) *vim.lsp.diagnostic.goto_prev()* + Move to the previous diagnostic + + Parameters: ~ + {opts} table See |vim.lsp.diagnostics.goto_next()| + + *vim.lsp.diagnostic.on_publish_diagnostics()* +on_publish_diagnostics({_}, {_}, {params}, {client_id}, {_}, {config}) + |lsp-handler| for the method "textDocument/publishDiagnostics" + + Note: + Each of the configuration options accepts: + • `false` : Disable this feature + • `true` : Enable this feature, use default settings. + • `table` : Enable this feature, use overrides. + • `function`: Function with signature (bufnr, client_id) that + returns any of the above.> + + vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( + vim.lsp.diagnostic.on_publish_diagnostics, { + -- Enable underline, use default values + underline = true, + -- Enable virtual text, override spacing to 4 + virtual_text = { + spacing = 4, + }, + -- Use a function to dynamically turn signs off + -- and on, using buffer local variables + signs = function(bufnr, client_id) + return vim.bo[bufnr].show_signs == false + end, + -- Disable a feature + update_in_insert = false, + } + ) +< - Members: - • {pid} (number) The LSP server's PID. - • {handle} A handle for low-level interaction with the LSP - server process |vim.loop|. + Parameters: ~ + {config} table Configuration table. + • underline: (default=true) + • Apply underlines to diagnostics. + • See |vim.lsp.diagnostic.set_underline()| + • virtual_text: (default=true) + • Apply virtual text to line endings. + • See |vim.lsp.diagnostic.set_virtual_text()| -============================================================================== -Lua module: vim.lsp.util *lsp-util* + • signs: (default=true) + • Apply signs for diagnostics. + • See |vim.lsp.diagnostic.set_signs()| - *vim.lsp.util.apply_text_document_edit()* -apply_text_document_edit({text_document_edit}) - Parameters: ~ - {text_document_edit} (table) a `TextDocumentEdit` object + • update_in_insert: (default=false) + • Update diagnostics in InsertMode or wait + until InsertLeave - See also: ~ - https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit +save({diagnostics}, {bufnr}, {client_id}) *vim.lsp.diagnostic.save()* + Save diagnostics to the current buffer. - *vim.lsp.util.apply_text_edits()* -apply_text_edits({text_edits}, {bufnr}) - Applies a list of text edits to a buffer. + Handles saving diagnostics from multiple clients in the same + buffer. Parameters: ~ - {text_edits} (table) list of `TextEdit` objects - {buf_nr} (number) Buffer id + {diagnostics} Diagnostic [] + {bufnr} number + {client_id} number - *vim.lsp.util.apply_workspace_edit()* -apply_workspace_edit({workspace_edit}) - Applies a `WorkspaceEdit` . +set_loclist({opts}) *vim.lsp.diagnostic.set_loclist()* + Sets the location list Parameters: ~ - {workspace_edit} (table) `WorkspaceEdit` + {opts} table|nil Configuration table. Keys: + • {open_loclist}: (boolean, default true) + • Open loclist after set + + • {client_id}: (number) + • If nil, will consider all clients attached to + buffer. + + • {severity}: (DiagnosticSeverity) + • Exclusive severity to consider. Overrides + {severity_limit} + + • {severity_limit}: (DiagnosticSeverity) + • Limit severity of diagnostics found. E.g. + "Warning" means { "Error", "Warning" } will be + valid. + + *vim.lsp.diagnostic.set_signs()* +set_signs({diagnostics}, {bufnr}, {client_id}, {sign_ns}, {opts}) + Set signs for given diagnostics + + Sign characters can be customized with the following commands: +> -buf_clear_diagnostics({bufnr}) *vim.lsp.util.buf_clear_diagnostics()* - Clears diagnostics for a buffer. + sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl= + sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl= + sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl= + sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl= +< Parameters: ~ - {bufnr} (number) buffer id + {diagnostics} Diagnostic [] + {bufnr} number The buffer number + {client_id} number the client id + {sign_ns} number|nil + {opts} table Configuration for signs. Keys: + • priority: Set the priority of the signs. -buf_clear_references({bufnr}) *vim.lsp.util.buf_clear_references()* - Removes document highlights from a buffer. + *vim.lsp.diagnostic.set_underline()* +set_underline({diagnostics}, {bufnr}, {client_id}, {diagnostic_ns}, {opts}) + Set underline for given diagnostics + + Underline highlights can be customized by changing the + following |:highlight| groups. +> + + LspDiagnosticsUnderlineError + LspDiagnosticsUnderlineWarning + LspDiagnosticsUnderlineInformation + LspDiagnosticsUnderlineHint +< Parameters: ~ - {bufnr} buffer id + {diagnostics} Diagnostic [] + {bufnr} number The buffer number + {client_id} number the client id + {diagnostic_ns} number|nil + {opts} table Currently unused. -buf_diagnostics_count({kind}) *vim.lsp.util.buf_diagnostics_count()* - Returns the number of diagnostics of given kind for current - buffer. + *vim.lsp.diagnostic.set_virtual_text()* +set_virtual_text({diagnostics}, {bufnr}, {client_id}, {diagnostic_ns}, {opts}) + Set virtual text given diagnostics - Useful for showing diagnostic counts in statusline. eg: + Virtual text highlights can be customized by changing the + following |:highlight| groups. > - function! LspStatus() abort - let sl = '' - if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))') - let sl.='%#MyStatuslineLSP#E:' - let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Error]])")}' - let sl.='%#MyStatuslineLSP# W:' - let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Warning]])")}' - else - let sl.='%#MyStatuslineLSPErrors#off' - endif - return sl - endfunction - let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus() + LspDiagnosticsVirtualTextError + LspDiagnosticsVirtualTextWarning + LspDiagnosticsVirtualTextInformation + LspDiagnosticsVirtualTextHint < Parameters: ~ - {kind} Diagnostic severity kind: See - |vim.lsp.protocol.DiagnosticSeverity| + {diagnostics} Diagnostic [] + {bufnr} number + {client_id} number + {diagnostic_ns} number + {opts} table Options on how to display virtual + text. Keys: + • prefix (string): Prefix to display + before virtual text on line + • spacing (number): Number of spaces to + insert before virtual text + + *vim.lsp.diagnostic.show_line_diagnostics()* +show_line_diagnostics({opts}, {bufnr}, {line_nr}, {client_id}) + Open a floating window with the diagnostics from {line_nr} + + The floating window can be customized with the following + highlight groups: > + + LspDiagnosticsFloatingError + LspDiagnosticsFloatingWarning + LspDiagnosticsFloatingInformation + LspDiagnosticsFloatingHint +< + + Parameters: ~ + {opts} table Configuration table + • show_header (boolean, default true): Show + "Diagnostics:" header. + {bufnr} number The buffer number + {line_nr} number The line number + {client_id} number|nil the client id Return: ~ - Count of diagnostics + {popup_bufnr, win_id} - *vim.lsp.util.buf_diagnostics_save_positions()* -buf_diagnostics_save_positions({bufnr}, {diagnostics}) - Saves diagnostics into - vim.lsp.util.diagnostics_by_buf[{bufnr}]. +============================================================================== +Lua module: vim.lsp.util *lsp-util* + + *vim.lsp.util.apply_text_document_edit()* +apply_text_document_edit({text_document_edit}) Parameters: ~ - {bufnr} (number) buffer id for which the - diagnostics are for - {diagnostics} list of `Diagnostic` s received from the - LSP server + {text_document_edit} (table) a `TextDocumentEdit` object - *vim.lsp.util.buf_diagnostics_signs()* -buf_diagnostics_signs({bufnr}, {diagnostics}) - Places signs for each diagnostic in the sign column. + See also: ~ + https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit - Sign characters can be customized with the following commands: -> - sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl= - sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl= - sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl= - sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl= -< + *vim.lsp.util.apply_text_edits()* +apply_text_edits({text_edits}, {bufnr}) + Applies a list of text edits to a buffer. + + Parameters: ~ + {text_edits} (table) list of `TextEdit` objects + {buf_nr} (number) Buffer id - *vim.lsp.util.buf_diagnostics_underline()* -buf_diagnostics_underline({bufnr}, {diagnostics}) - Highlights a list of diagnostics in a buffer by underlining - them. + *vim.lsp.util.apply_workspace_edit()* +apply_workspace_edit({workspace_edit}) + Applies a `WorkspaceEdit` . Parameters: ~ - {bufnr} (number) buffer id - {diagnostics} (list of `Diagnostic` s) + {workspace_edit} (table) `WorkspaceEdit` - *vim.lsp.util.buf_diagnostics_virtual_text()* -buf_diagnostics_virtual_text({bufnr}, {diagnostics}) - Given a list of diagnostics, sets the corresponding virtual - text for a buffer. +buf_clear_references({bufnr}) *vim.lsp.util.buf_clear_references()* + Removes document highlights from a buffer. Parameters: ~ - {bufnr} buffer id - {diagnostics} (table) list of `Diagnostic` s + {bufnr} buffer id *vim.lsp.util.buf_highlight_references()* buf_highlight_references({bufnr}, {references}) @@ -1146,20 +1453,6 @@ convert_signature_help_to_markdown_lines({signature_help}) See also: ~ https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp - *vim.lsp.util.diagnostics_group_by_line()* -diagnostics_group_by_line({diagnostics}) - Groups a list of diagnostics by line. - - Parameters: ~ - {diagnostics} (table) list of `Diagnostic` s - - Return: ~ - (table) dictionary mapping lines to lists of diagnostics - valid on those lines - - See also: ~ - https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic - *vim.lsp.util.extract_completion_items()* extract_completion_items({result}) Can be used to extract the completion items from a `textDocument/completion` request, which may return one of `CompletionItem[]` , `CompletionList` or null. @@ -1185,6 +1478,21 @@ fancy_floating_markdown({contents}, {opts}) Parameters: ~ {contents} table of lines to show in window {opts} dictionary with optional fields + • height of floating window + • width of floating window + • wrap_at character to wrap at for computing + height + • max_width maximal width of floating window + • max_height maximal height of floating window + • pad_left number of columns to pad contents + at left + • pad_right number of columns to pad contents + at right + • pad_top number of lines to pad contents at + top + • pad_bottom number of lines to pad contents + at bottom + • separator insert separator after code block Return: ~ width,height size of float @@ -1231,26 +1539,6 @@ get_effective_tabstop({bufnr}) *vim.lsp.util.get_effective_tabstop()* See also: ~ |softtabstop| -get_line_diagnostics() *vim.lsp.util.get_line_diagnostics()* - Gets list of diagnostics for the current line. - - Return: ~ - (table) list of `Diagnostic` tables - - See also: ~ - https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic - - *vim.lsp.util.get_severity_highlight_name()* -get_severity_highlight_name({severity}) - Gets the name of a severity's highlight group. - - Parameters: ~ - {severity} A member of - `vim.lsp.protocol.DiagnosticSeverity` - - Return: ~ - (string) Highlight group name - jump_to_location({location}) *vim.lsp.util.jump_to_location()* Jumps to a location. @@ -1414,10 +1702,6 @@ set_qflist({items}) *vim.lsp.util.set_qflist()* Parameters: ~ {items} (table) list of items -show_line_diagnostics() *vim.lsp.util.show_line_diagnostics()* - Displays the diagnostics for the current line in a floating - hover window. - symbols_to_items({symbols}, {bufnr}) *vim.lsp.util.symbols_to_items()* Converts symbols to quickfix list items. @@ -1465,4 +1749,134 @@ try_trim_markdown_code_blocks({lines}) Return: ~ (string) filetype or 'markdown' if it was unchanged. + +============================================================================== +Lua module: vim.lsp.log *lsp-log* + +get_filename() *vim.lsp.log.get_filename()* + Returns the log filename. + + Return: ~ + (string) log filename + +set_level({level}) *vim.lsp.log.set_level()* + Sets the current log level. + + Parameters: ~ + {level} (string or number) One of `vim.lsp.log.levels` + +should_log({level}) *vim.lsp.log.should_log()* + Checks whether the level is sufficient for logging. + + Parameters: ~ + {level} number log level + + Return: ~ + (bool) true if would log, false if not + + +============================================================================== +Lua module: vim.lsp.rpc *lsp-rpc* + +format_rpc_error({err}) *vim.lsp.rpc.format_rpc_error()* + Constructs an error message from an LSP error object. + + Parameters: ~ + {err} (table) The error object + + Return: ~ + (string) The formatted error message + +notify({method}, {params}) *vim.lsp.rpc.notify()* + Sends a notification to the LSP server. + + Parameters: ~ + {method} (string) The invoked LSP method + {params} (table): Parameters for the invoked LSP method + + Return: ~ + (bool) `true` if notification could be sent, `false` if + not + +request({method}, {params}, {callback}) *vim.lsp.rpc.request()* + Sends a request to the LSP server and runs {callback} upon + response. + + Parameters: ~ + {method} (string) The invoked LSP method + {params} (table) Parameters for the invoked LSP method + {callback} (function) Callback to invoke + + Return: ~ + (bool, number) `(true, message_id)` if request could be + sent, `false` if not + + *vim.lsp.rpc.rpc_response_error()* +rpc_response_error({code}, {message}, {data}) + Creates an RPC response object/table. + + Parameters: ~ + {code} RPC error code defined in + `vim.lsp.protocol.ErrorCodes` + {message} (optional) arbitrary message to send to server + {data} (optional) arbitrary data to send to server + + *vim.lsp.rpc.start()* +start({cmd}, {cmd_args}, {dispatchers}, {extra_spawn_params}) + Starts an LSP server process and create an LSP RPC client + object to interact with it. + + Parameters: ~ + {cmd} (string) Command to start the LSP + server. + {cmd_args} (table) List of additional string + arguments to pass to {cmd}. + {dispatchers} (table, optional) Dispatchers for + LSP message types. Valid dispatcher + names are: + • `"notification"` + • `"server_request"` + • `"on_error"` + • `"on_exit"` + {extra_spawn_params} (table, optional) Additional context + for the LSP server process. May + contain: + • {cwd} (string) Working directory + for the LSP server process + • {env} (table) Additional + environment variables for LSP + server process + + Return: ~ + Client RPC object. + Methods: + • `notify()` |vim.lsp.rpc.notify()| + • `request()` |vim.lsp.rpc.request()| + + Members: + • {pid} (number) The LSP server's PID. + • {handle} A handle for low-level interaction with the LSP + server process |vim.loop|. + + +============================================================================== +Lua module: vim.lsp.protocol *lsp-protocol* + + *vim.lsp.protocol.make_client_capabilities()* +make_client_capabilities() + Gets a new ClientCapabilities object describing the LSP client + capabilities. + + *vim.lsp.protocol.resolve_capabilities()* +resolve_capabilities({server_capabilities}) + `*` to match one or more characters in a path segment `?` to + match on one character in a path segment `**` to match any + number of path segments, including none `{}` to group + conditions (e.g. `**/*.{ts,js}` matches all TypeScript and + JavaScript files) `[]` to declare a range of characters to + match in a path segment (e.g., `example.[0-9]` to match on + `example.0` , `example.1` , …) `[!...]` to negate a range of + characters to match in a path segment (e.g., `example.[!0-9]` + to match on `example.a` , `example.b` , but not `example.0` ) + vim:tw=78:ts=8:ft=help:norl: diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 9f537caee8..a03de10a17 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1325,6 +1325,7 @@ uri_to_bufnr({uri}) *vim.uri_to_bufnr()* Return: ~ bufnr. + Note: Creates buffer but does not load it diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua new file mode 100644 index 0000000000..5887e978b9 --- /dev/null +++ b/runtime/lua/vim/F.lua @@ -0,0 +1,24 @@ +local F = {} + +--- Returns {a} if it is not nil, otherwise returns {b}. +--- +--@param a +--@param b +function F.if_nil(a, b) + if a == nil then return b end + return a +end + +-- Use in combination with pcall +function F.ok_or_nil(status, ...) + if not status then return end + return ... +end + +-- Nil pcall. +function F.npcall(fn, ...) + return F.ok_or_nil(pcall(fn, ...)) +end + + +return F diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua index 705b34dc99..0012dce081 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/highlight.lua @@ -2,6 +2,22 @@ local api = vim.api local highlight = {} +--@private +function highlight.create(higroup, hi_info, default) + local options = {} + -- TODO: Add validation + for k, v in pairs(hi_info) do + table.insert(options, string.format("%s=%s", k, v)) + end + vim.cmd(string.format([[highlight %s %s %s]], default and "default" or "", higroup, table.concat(options, " "))) +end + +--@private +function highlight.link(higroup, link_to, force) + vim.cmd(string.format([[highlight%s link %s %s]], force and "!" or " default", higroup, link_to)) +end + + --- Highlight range between two positions --- --@param bufnr number of buffer to apply highlighting to diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 1a0015e2db..dacdbcfa17 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,4 +1,4 @@ -local default_callbacks = require 'vim.lsp.callbacks' +local default_handlers = require 'vim.lsp.handlers' local log = require 'vim.lsp.log' local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' @@ -13,16 +13,21 @@ local validate = vim.validate local lsp = { protocol = protocol; - callbacks = default_callbacks; + + -- TODO(tjdevries): Add in the warning that `callbacks` is no longer supported. + -- util.warn_once("vim.lsp.callbacks is deprecated. Use vim.lsp.handlers instead.") + handlers = default_handlers; + callbacks = default_handlers; + buf = require'vim.lsp.buf'; + diagnostic = require'vim.lsp.diagnostic'; util = util; + -- Allow raw RPC access. rpc = lsp_rpc; + -- Export these directly from rpc. rpc_response_error = lsp_rpc.rpc_response_error; - -- You probably won't need this directly, since __tostring is set for errors - -- by the RPC. - -- format_rpc_error = lsp_rpc.format_rpc_error; } -- maps request name to the required resolved_capability in the client. @@ -72,7 +77,7 @@ local function resolve_bufnr(bufnr) end --@private ---- callback called by the client when trying to call a method that's not +--- 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) @@ -115,14 +120,14 @@ local all_buffer_active_clients = {} local uninitialized_clients = {} --@private ---- Invokes a callback for each LSP client attached to the buffer {bufnr}. +--- Invokes a function for each LSP client attached to the buffer {bufnr}. --- --@param bufnr (Number) of buffer ---@param callback (function({client}, {client_id}, {bufnr}) Function to run on +--@param fn (function({client}, {client_id}, {bufnr}) Function to run on ---each client attached to that buffer. -local function for_each_buffer_client(bufnr, callback) +local function for_each_buffer_client(bufnr, fn) validate { - callback = { callback, 'f' }; + fn = { fn, 'f' }; } bufnr = resolve_bufnr(bufnr) local client_ids = all_buffer_active_clients[bufnr] @@ -132,7 +137,7 @@ local function for_each_buffer_client(bufnr, callback) for client_id in pairs(client_ids) do local client = active_clients[client_id] if client then - callback(client, client_id, bufnr) + fn(client, client_id, bufnr) end end end @@ -209,7 +214,9 @@ local function validate_client_config(config) } validate { root_dir = { config.root_dir, is_dir, "directory" }; + -- TODO(remove-callbacks) callbacks = { config.callbacks, "t", true }; + handlers = { config.handlers, "t", true }; capabilities = { config.capabilities, "t", true }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; cmd_env = { config.cmd_env, "t", true }; @@ -220,13 +227,23 @@ local function validate_client_config(config) before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; } + + -- TODO(remove-callbacks) + if config.handlers and config.callbacks then + error(debug.traceback( + "Unable to configure LSP with both 'config.handlers' and 'config.callbacks'. Use 'config.handlers' exclusively." + )) + end + local cmd, cmd_args = lsp._cmd_parts(config.cmd) local offset_encoding = valid_encodings.UTF16 if config.offset_encoding then offset_encoding = validate_encoding(config.offset_encoding) end + return { - cmd = cmd; cmd_args = cmd_args; + cmd = cmd; + cmd_args = cmd_args; offset_encoding = offset_encoding; } end @@ -276,12 +293,11 @@ end --- --- - Methods: --- ---- - request(method, params, [callback], bufnr) +--- - request(method, params, [handler], bufnr) --- Sends a request to the server. --- This is a thin wrapper around {client.rpc.request} with some additional --- checking. ---- If {callback} is not specified, it will use {client.callbacks} to try to ---- find a callback. If one is not found there, then an error will occur. +--- If {handler} is not specified, If one is not found there, then an error will occur. --- Returns: {status}, {[client_id]}. {status} is a boolean indicating if --- the notification was successful. If it is `false`, then it will always --- be `false` (the client has shutdown). @@ -325,8 +341,7 @@ end --- with the server. You can modify this in the `config`'s `on_init` method --- before text is sent to the server. --- ---- - {callbacks} (table): The callbacks used by the client as ---- described in |lsp-callbacks|. +--- - {handlers} (table): The handlers used by the client as described in |lsp-handler|. --- --- - {config} (table): copy of the table that was passed by the user --- to |vim.lsp.start_client()|. @@ -378,15 +393,7 @@ end --- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an --- array. --- ---@param callbacks Map of language server method names to ---- `function(err, method, params, client_id)` handler. Invoked for: ---- - Notifications to the server, where `err` will always be `nil`. ---- - Requests by the server. For these you can respond by returning ---- two values: `result, err` where err must be shaped like a RPC error, ---- i.e. `{ code, message, data? }`. Use |vim.lsp.rpc_response_error()| to ---- help with this. ---- - Default callback for client requests not explicitly specifying ---- a callback. +--@param handlers Map of language server method names to |lsp-handler| --- --@param init_options Values to pass in the initialization request --- as `initializationOptions`. See `initialize` in the LSP spec. @@ -437,52 +444,51 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = config.callbacks or {} + -- TODO(remove-callbacks) + local handlers = config.handlers or config.callbacks or {} local name = config.name or tostring(client_id) local log_prefix = string.format("LSP[%s]", name) - local handlers = {} + local dispatch = {} --@private - --- Returns the callback associated with an LSP method. Returns the default - --- callback if the user hasn't set a custom one. + --- Returns the handler associated with an LSP method. + --- Returns the default handler if the user hasn't set a custom one. --- --@param method (string) LSP method name - --@returns (fn) The callback for the given method, if defined, or the default - ---from |lsp-callbacks| - local function resolve_callback(method) - return callbacks[method] or default_callbacks[method] + --@returns (fn) The handler for the given method, if defined, or the default from |vim.lsp.handlers| + local function resolve_handler(method) + return handlers[method] or default_handlers[method] end --@private --- Handles a notification sent by an LSP server by invoking the - --- corresponding callback. + --- corresponding handler. --- --@param method (string) LSP method name --@param params (table) The parameters for that method. - function handlers.notification(method, params) + function dispatch.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) - local callback = resolve_callback(method) - if callback then + local handler = resolve_handler(method) + if handler then -- Method name is provided here for convenience. - callback(nil, method, params, client_id) + handler(nil, method, params, client_id) end end --@private - --- Handles a request from an LSP server by invoking the corresponding - --- callback. + --- Handles a request from an LSP server by invoking the corresponding handler. --- --@param method (string) LSP method name --@param params (table) The parameters for that method - function handlers.server_request(method, params) + function dispatch.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) - local callback = resolve_callback(method) - if callback then - local _ = log.debug() and log.debug("server_request: found callback for", method) - return callback(nil, method, params, client_id) + local handler = resolve_handler(method) + if handler then + local _ = log.debug() and log.debug("server_request: found handler for", method) + return handler(nil, method, params, client_id) end - local _ = log.debug() and log.debug("server_request: no callback found for", method) + local _ = log.debug() and log.debug("server_request: no handler found for", method) return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) end @@ -493,7 +499,7 @@ function lsp.start_client(config) --@param err (...) Other arguments may be passed depending on the error kind --@see |vim.lsp.client_errors| for possible errors. Use ---`vim.lsp.client_errors[code]` to get a human-friendly name. - function handlers.on_error(code, err) + function dispatch.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) if config.on_error then @@ -510,7 +516,7 @@ function lsp.start_client(config) --- --@param code (number) exit code of the process --@param signal (number) the signal used to terminate (if any) - function handlers.on_exit(code, signal) + function dispatch.on_exit(code, signal) active_clients[client_id] = nil uninitialized_clients[client_id] = nil local active_buffers = {} @@ -523,7 +529,7 @@ function lsp.start_client(config) -- Buffer level cleanup vim.schedule(function() for _, bufnr in ipairs(active_buffers) do - util.buf_clear_diagnostics(bufnr) + lsp.diagnostic.clear(bufnr) end end) if config.on_exit then @@ -532,7 +538,7 @@ function lsp.start_client(config) end -- Start the RPC client. - local rpc = lsp_rpc.start(cmd, cmd_args, handlers, { + local rpc = lsp_rpc.start(cmd, cmd_args, dispatch, { cwd = config.cmd_cwd; env = config.cmd_env; }) @@ -542,12 +548,14 @@ function lsp.start_client(config) name = name; rpc = rpc; offset_encoding = offset_encoding; - callbacks = callbacks; config = config; + + -- TODO(remove-callbacks) + callbacks = handlers; + handlers = handlers; } - -- Store the uninitialized_clients for cleanup in case we exit before - -- initialize finishes. + -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. uninitialized_clients[client_id] = client; --@private @@ -641,13 +649,11 @@ function lsp.start_client(config) --- Sends a request to the server. --- --- This is a thin wrapper around {client.rpc.request} with some additional - --- checks for capabilities and callback availability. + --- checks for capabilities and handler availability. --- --@param method (string) LSP method name. --@param params (table) LSP request params. - --@param callback (function, optional) Response handler for this method. - ---If {callback} is not specified, it will use {client.callbacks} to try to - ---find a callback. If one is not found there, then an error will occur. + --@param handler (function, optional) Response |lsp-handler| for this method. --@param bufnr (number) Buffer handle (0 for current). --@returns ({status}, [request_id]): {status} is a bool indicating ---whether the request was successful. If it is `false`, then it will @@ -656,16 +662,14 @@ function lsp.start_client(config) ---second result. You can use this with `client.cancel_request(request_id)` ---to cancel the-request. --@see |vim.lsp.buf_request()| - function client.request(method, params, callback, bufnr) - -- FIXME: callback is optional, but bufnr is apparently not? Shouldn't that - -- require a `select('#', ...)` call? - if not callback then - callback = resolve_callback(method) - or error(string.format("not found: %q request callback for client %q.", method, client.name)) + function client.request(method, params, handler, bufnr) + if not handler then + handler = resolve_handler(method) + or error(string.format("not found: %q request handler for client %q.", method, client.name)) end - local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr) + local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) return rpc.request(method, params, function(err, result) - callback(err, method, result, client_id, bufnr) + handler(err, method, result, client_id, bufnr) end) end @@ -995,19 +999,18 @@ nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()") --@param bufnr (number) Buffer handle, or 0 for current. --@param method (string) LSP method name --@param params (optional, table) Parameters to send to the server ---@param callback (optional, functionnil) Handler --- `function(err, method, params, client_id)` for this request. Defaults --- to the client callback in `client.callbacks`. See |lsp-callbacks|. +--@param handler (optional, function) See |lsp-handler| +-- If nil, follows resolution strategy defined in |lsp-handler-configuration| -- --@returns 2-tuple: --- - Map of client-id:request-id pairs for all successful requests. --- - Function which can be used to cancel all the requests. You could instead --- iterate all clients and call their `cancel_request()` methods. -function lsp.buf_request(bufnr, method, params, callback) +function lsp.buf_request(bufnr, method, params, handler) validate { bufnr = { bufnr, 'n', true }; method = { method, 's' }; - callback = { callback, 'f', true }; + handler = { handler, 'f', true }; } local client_request_ids = {} @@ -1015,7 +1018,7 @@ function lsp.buf_request(bufnr, method, params, callback) 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) + local request_success, request_id = client.request(method, params, handler, 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. @@ -1025,13 +1028,13 @@ function lsp.buf_request(bufnr, method, params, callback) end end) - -- if no clients support the given method, call the callback with the proper + -- if no clients support the given method, call the handler 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) + handler = handler or lsp.handlers[method] + if handler then + handler(unsupported_err, method, bufnr) end return end @@ -1064,11 +1067,11 @@ end function lsp.buf_request_sync(bufnr, method, params, timeout_ms) local request_results = {} local result_count = 0 - local function _callback(err, _method, result, client_id) + local function _sync_handler(err, _, result, client_id) request_results[client_id] = { error = err, result = result } result_count = result_count + 1 end - local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _callback) + local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _sync_handler) local expected_result_count = 0 for _ in pairs(client_request_ids) do expected_result_count = expected_result_count + 1 @@ -1209,22 +1212,53 @@ function lsp.get_log_path() return log.get_filename() end --- Defines the LspDiagnostics signs if they're not defined already. -do - --@private - --- Defines a sign if it isn't already defined. - --@param name (String) Name of the sign - --@param properties (table) Properties to attach to the sign - local function define_default_sign(name, properties) - if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then - vim.fn.sign_define(name, properties) +--- Call {fn} for every client attached to {bufnr} +function lsp.for_each_buffer_client(bufnr, fn) + return for_each_buffer_client(bufnr, fn) +end + +--- Function to manage overriding defaults for LSP handlers. +--@param handler (function) See |lsp-handler| +--@param override_config (table) Table containing the keys to override behavior of the {handler} +function lsp.with(handler, override_config) + return function(err, method, params, client_id, bufnr, config) + return handler(err, method, params, client_id, bufnr, vim.tbl_deep_extend("force", config or {}, override_config)) + end +end + +--- Helper function to use when implementing a handler. +--- This will check that all of the keys in the user configuration +--- are valid keys and make sense to include for this handler. +--- +--- Will error on invalid keys (i.e. keys that do not exist in the options) +function lsp._with_extend(name, options, user_config) + user_config = user_config or {} + + local resulting_config = {} + for k, v in pairs(user_config) do + if options[k] == nil then + error(debug.traceback(string.format( + "Invalid option for `%s`: %s. Valid options are:\n%s", + name, + k, + vim.inspect(vim.tbl_keys(options)) + ))) end + + resulting_config[k] = v end - define_default_sign('LspDiagnosticsErrorSign', {text='E', texthl='LspDiagnosticsErrorSign', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsWarningSign', {text='W', texthl='LspDiagnosticsWarningSign', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsInformationSign', {text='I', texthl='LspDiagnosticsInformationSign', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsHintSign', {text='H', texthl='LspDiagnosticsHintSign', linehl='', numhl=''}) + + for k, v in pairs(options) do + if resulting_config[k] == nil then + resulting_config[k] = v + end + end + + return resulting_config end +-- Define the LspDiagnostics signs if they're not defined already. +require('vim.lsp.diagnostic')._define_default_signs_and_highlights() + return lsp -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 0b8e08f36c..fa62905c0a 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -30,9 +30,7 @@ end --- --@param method (string) LSP method name --@param params (optional, table) Parameters to send to the server ---@param callback (optional, functionnil) Handler --- `function(err, method, params, client_id)` for this request. Defaults --- to the client callback in `client.callbacks`. See |lsp-callbacks|. +--@param handler (optional, functionnil) See |lsp-handler|. Follows |lsp-handler-resolution| -- --@returns 2-tuple: --- - Map of client-id:request-id pairs for all successful requests. @@ -40,12 +38,12 @@ end --- iterate all clients and call their `cancel_request()` methods. --- --@see |vim.lsp.buf_request()| -local function request(method, params, callback) +local function request(method, params, handler) validate { method = {method, 's'}; - callback = {callback, 'f', true}; + handler = {handler, 'f', true}; } - return vim.lsp.buf_request(0, method, params, callback) + return vim.lsp.buf_request(0, method, params, handler) end --- Checks whether the language servers attached to the current buffer are @@ -64,6 +62,7 @@ function M.hover() end --- Jumps to the declaration of the symbol under the cursor. +--@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead. --- function M.declaration() local params = util.make_position_params() @@ -279,7 +278,7 @@ end --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction function M.code_action(context) validate { context = { context, 't', true } } - context = context or { diagnostics = util.get_line_diagnostics() } + context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() } local params = util.make_range_params() params.context = context request('textDocument/codeAction', params) @@ -294,7 +293,7 @@ end ---Defaults to the end of the last visual selection. function M.range_code_action(context, start_pos, end_pos) validate { context = { context, 't', true } } - context = context or { diagnostics = util.get_line_diagnostics() } + context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() } local params = util.make_given_range_params(start_pos, end_pos) params.context = context request('textDocument/codeAction', params) diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 3270d1d2a9..1da92b900d 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -1,345 +1,4 @@ -local log = require 'vim.lsp.log' -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 = {} - --- FIXME: DOC: Expose in vimdocs - ---@private ---- Writes to error buffer. ---@param ... (table of strings) Will be concatenated before being written -local function err_message(...) - api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) - api.nvim_command("redraw") -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand -M['workspace/executeCommand'] = function(err, _) - if err then - error("Could not execute code action: "..err.message) - end -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction -M['textDocument/codeAction'] = function(_, _, actions) - if actions == nil or vim.tbl_isempty(actions) then - print("No code actions available") - return - end - - local option_strings = {"Code Actions:"} - for i, action in ipairs(actions) do - local title = action.title:gsub('\r\n', '\\r\\n') - title = title:gsub('\n', '\\n') - table.insert(option_strings, string.format("%d. %s", i, title)) - end - - local choice = vim.fn.inputlist(option_strings) - if choice < 1 or choice > #actions then - return - end - local action_chosen = actions[choice] - -- textDocument/codeAction can return either Command[] or CodeAction[]. - -- If it is a CodeAction, it can have either an edit, a command or both. - -- Edits should be executed first - if action_chosen.edit or type(action_chosen.command) == "table" then - if action_chosen.edit then - util.apply_workspace_edit(action_chosen.edit) - end - if type(action_chosen.command) == "table" then - buf.execute_command(action_chosen.command) - end - else - buf.execute_command(action_chosen) - end -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 - -- TODO(ashkan) Do something more with label? - if workspace_edit.label then - print("Workspace edit", workspace_edit.label) - end - local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit) - return { - applied = status; - failureReason = result; - } -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_publishDiagnostics -M['textDocument/publishDiagnostics'] = function(_, _, result) - if not result then return end - local uri = result.uri - local bufnr = vim.uri_to_bufnr(uri) - if not bufnr then - err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri) - return - end - - -- 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. - -- TODO: Replace this with server-specific heuristics to infer severity. - for _, diagnostic in ipairs(result.diagnostics) do - if diagnostic.severity == nil then - diagnostic.severity = protocol.DiagnosticSeverity.Error - 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) - vim.api.nvim_command("doautocmd User LspDiagnosticsChanged") -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references -M['textDocument/references'] = function(_, _, result) - if not result then return end - util.set_qflist(util.locations_to_items(result)) - api.nvim_command("copen") - api.nvim_command("wincmd p") -end - ---@private ---- Prints given list of symbols to the quickfix list. ---@param _ (not used) ---@param _ (not used) ---@param result (list of Symbols) LSP method name ---@param result (table) result of LSP method; a location or a list of locations. ----(`textDocument/definition` can return `Location` or `Location[]` -local symbol_callback = function(_, _, result, _, bufnr) - if not result or vim.tbl_isempty(result) then return end - - util.set_qflist(util.symbols_to_items(result, bufnr)) - api.nvim_command("copen") - api.nvim_command("wincmd p") -end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol -M['textDocument/documentSymbol'] = symbol_callback ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol -M['workspace/symbol'] = symbol_callback - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename -M['textDocument/rename'] = function(_, _, result) - if not result then return end - util.apply_workspace_edit(result) -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting -M['textDocument/rangeFormatting'] = function(_, _, result) - if not result then return end - util.apply_text_edits(result) -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting -M['textDocument/formatting'] = function(_, _, result) - if not result then return end - util.apply_text_edits(result) -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion -M['textDocument/completion'] = function(_, _, result) - if vim.tbl_isempty(result or {}) then return end - local row, col = unpack(api.nvim_win_get_cursor(0)) - local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) - local line_to_cursor = line:sub(col+1) - local textMatch = vim.fn.match(line_to_cursor, '\\k*$') - local prefix = line_to_cursor:sub(textMatch+1) - - local matches = util.text_document_completion_list_to_complete_items(result, prefix) - vim.fn.complete(textMatch+1, matches) -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover -M['textDocument/hover'] = function(_, method, result) - util.focusable_float(method, function() - if not (result and result.contents) then - -- return { 'No information available' } - return - end - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = util.trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then - -- return { 'No information available' } - return - end - local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, { - pad_left = 1; pad_right = 1; - }) - util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr) - return bufnr, winnr - end) -end - ---@private ---- Jumps to a location. Used as a callback for multiple LSP methods. ---@param _ (not used) ---@param method (string) LSP method name ---@param result (table) result of LSP method; a location or a list of locations. ----(`textDocument/definition` can return `Location` or `Location[]` -local function location_callback(_, method, result) - if result == nil or vim.tbl_isempty(result) then - local _ = log.info() and log.info(method, 'No location found') - return nil - end - - -- textDocument/definition can return Location or Location[] - -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition - - if vim.tbl_islist(result) then - util.jump_to_location(result[1]) - - if #result > 1 then - util.set_qflist(util.locations_to_items(result)) - api.nvim_command("copen") - api.nvim_command("wincmd p") - end - else - util.jump_to_location(result) - end -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration -M['textDocument/declaration'] = location_callback ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition -M['textDocument/definition'] = location_callback ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition -M['textDocument/typeDefinition'] = location_callback ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation -M['textDocument/implementation'] = location_callback - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp -M['textDocument/signatureHelp'] = function(_, method, result) - -- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp callback - -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore - if not (result and result.signatures and result.signatures[1]) then - print('No signature help available') - return - end - local lines = util.convert_signature_help_to_markdown_lines(result) - lines = util.trim_empty_lines(lines) - if vim.tbl_isempty(lines) then - print('No signature help available') - return - end - util.focusable_preview(method, function() - return lines, util.try_trim_markdown_code_blocks(lines) - end) -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight -M['textDocument/documentHighlight'] = function(_, _, result, _) - if not result then return end - local bufnr = api.nvim_get_current_buf() - util.buf_highlight_references(bufnr, result) -end - ---@private ---- ---- Displays call hierarchy in the quickfix window. ---- ---@param direction `"from"` for incoming calls and `"to"` for outgoing calls ---@returns `CallHierarchyIncomingCall[]` if {direction} is `"from"`, ---@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`, -local make_call_hierarchy_callback = function(direction) - return function(_, _, result) - if not result then return end - local items = {} - for _, call_hierarchy_call in pairs(result) do - local call_hierarchy_item = call_hierarchy_call[direction] - for _, range in pairs(call_hierarchy_call.fromRanges) do - table.insert(items, { - filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)), - text = call_hierarchy_item.name, - lnum = range.start.line + 1, - col = range.start.character + 1, - }) - end - end - util.set_qflist(items) - api.nvim_command("copen") - api.nvim_command("wincmd p") - end -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/incomingCalls -M['callHierarchy/incomingCalls'] = make_call_hierarchy_callback('from') - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls -M['callHierarchy/outgoingCalls'] = make_call_hierarchy_callback('to') - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/logMessage -M['window/logMessage'] = function(_, _, result, client_id) - local message_type = result.type - local message = result.message - local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) - if not client then - err_message("LSP[", client_name, "] client has shut down after sending the message") - end - if message_type == protocol.MessageType.Error then - log.error(message) - elseif message_type == protocol.MessageType.Warning then - log.warn(message) - elseif message_type == protocol.MessageType.Info then - log.info(message) - else - log.debug(message) - end - return result -end - ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/showMessage -M['window/showMessage'] = function(_, _, result, client_id) - local message_type = result.type - local message = result.message - local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) - if not client then - err_message("LSP[", client_name, "] client has shut down after sending the message") - end - if message_type == protocol.MessageType.Error then - err_message("LSP[", client_name, "] ", message) - else - local message_type_name = protocol.MessageType[message_type] - api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) - end - return result -end - --- Add boilerplate error validation and logging for all of these. -for k, fn in pairs(M) do - M[k] = function(err, method, params, client_id, bufnr) - log.debug('default_callback', method, { params = params, client_id = client_id, err = err, bufnr = bufnr }) - if err then - error(tostring(err)) - end - return fn(err, method, params, client_id, bufnr) - end -end - -return M --- vim:sw=2 ts=2 et +util._warn_once("require('vim.lsp.callbacks') is deprecated. Use vim.lsp.handlers instead.") +return require('vim.lsp.handlers') diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua new file mode 100644 index 0000000000..590d694826 --- /dev/null +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -0,0 +1,1195 @@ +local api = vim.api +local validate = vim.validate + +local highlight = vim.highlight +local log = require('vim.lsp.log') +local protocol = require('vim.lsp.protocol') +local util = require('vim.lsp.util') + +local if_nil = vim.F.if_nil + +--@class DiagnosticSeverity +local DiagnosticSeverity = protocol.DiagnosticSeverity + +local to_severity = function(severity) + if not severity then return nil end + return type(severity) == 'string' and DiagnosticSeverity[severity] or severity +end + +local to_position = function(position, bufnr) + vim.validate { position = {position, 't'} } + + return { + position.line, + util._get_line_byte_from_position(bufnr, position) + } +end + + +---@brief lsp-diagnostic +--- +--@class Diagnostic +--@field range Range +--@field message string +--@field severity DiagnosticSeverity|nil +--@field code number | string +--@field source string +--@field tags DiagnosticTag[] +--@field relatedInformation DiagnosticRelatedInformation[] + +local M = {} + +-- Diagnostic Highlights {{{ + +-- TODO(tjdevries): Determine how to generate documentation for these +-- and how to configure them to be easy for users. +-- +-- For now, just use the following script. It should work pretty good. +--[[ +local levels = {"Error", "Warning", "Information", "Hint" } + +local all_info = { + { "Default", "Used as the base highlight group, other highlight groups link to", }, + { "VirtualText", 'Used for "%s" diagnostic virtual text.\n See |vim.lsp.diagnostic.set_virtual_text()|', }, + { "Underline", 'Used to underline "%s" diagnostics.\n See |vim.lsp.diagnostic.set_underline()|', }, + { "Floating", 'Used to color "%s" diagnostic messages in diagnostics float.\n See |vim.lsp.diagnostic.show_line_diagnostics()|', }, + { "Sign", 'Used for "%s" signs in sing column.\n See |vim.lsp.diagnostic.set_signs()|', }, +} + +local results = {} +for _, info in ipairs(all_info) do + for _, level in ipairs(levels) do + local name = info[1] + local description = info[2] + local fullname = string.format("Lsp%s%s", name, level) + table.insert(results, string.format( + "%78s", string.format("*hl-%s*", fullname)) + ) + + table.insert(results, fullname) + table.insert(results, string.format(" %s", description)) + table.insert(results, "") + end +end + +-- print(table.concat(results, '\n')) +vim.fn.setreg("*", table.concat(results, '\n')) +--]] + +local diagnostic_severities = { + [DiagnosticSeverity.Error] = { guifg = "Red" }; + [DiagnosticSeverity.Warning] = { guifg = "Orange" }; + [DiagnosticSeverity.Information] = { guifg = "LightBlue" }; + [DiagnosticSeverity.Hint] = { guifg = "LightGrey" }; +} + +-- Make a map from DiagnosticSeverity -> Highlight Name +local make_highlight_map = function(base_name) + local result = {} + for k, _ in pairs(diagnostic_severities) do + result[k] = "LspDiagnostics" .. base_name .. DiagnosticSeverity[k] + end + + return result +end + +local default_highlight_map = make_highlight_map("Default") +local virtual_text_highlight_map = make_highlight_map("VirtualText") +local underline_highlight_map = make_highlight_map("Underline") +local floating_highlight_map = make_highlight_map("Floating") +local sign_highlight_map = make_highlight_map("Sign") + +-- }}} +-- Diagnostic Namespaces {{{ +local DEFAULT_CLIENT_ID = -1 +local get_client_id = function(client_id) + if client_id == nil then + client_id = DEFAULT_CLIENT_ID + end + + return client_id +end + +local get_bufnr = function(bufnr) + if not bufnr then + return api.nvim_get_current_buf() + elseif bufnr == 0 then + return api.nvim_get_current_buf() + end + + return bufnr +end + + +--- Create a namespace table, used to track a client's buffer local items +local _make_namespace_table = function(namespace, api_namespace) + vim.validate { namespace = { namespace, 's' } } + + return setmetatable({ + [DEFAULT_CLIENT_ID] = api.nvim_create_namespace(namespace) + }, { + __index = function(t, client_id) + client_id = get_client_id(client_id) + + if rawget(t, client_id) == nil then + local value = string.format("%s:%s", namespace, client_id) + + if api_namespace then + value = api.nvim_create_namespace(value) + end + + rawset(t, client_id, value) + end + + return rawget(t, client_id) + end + }) +end + +local _diagnostic_namespaces = _make_namespace_table("vim_lsp_diagnostics", true) +local _sign_namespaces = _make_namespace_table("vim_lsp_signs", false) + +--@private +function M._get_diagnostic_namespace(client_id) + return _diagnostic_namespaces[client_id] +end + +--@private +function M._get_sign_namespace(client_id) + return _sign_namespaces[client_id] +end +-- }}} +-- Diagnostic Buffer & Client metatables {{{ +local bufnr_and_client_cacher_mt = { + __index = function(t, bufnr) + if bufnr == 0 or bufnr == nil then + bufnr = vim.api.nvim_get_current_buf() + end + + if rawget(t, bufnr) == nil then + rawset(t, bufnr, {}) + end + + return rawget(t, bufnr) + end, + + __newindex = function(t, bufnr, v) + if bufnr == 0 or bufnr == nil then + bufnr = vim.api.nvim_get_current_buf() + end + + rawset(t, bufnr, v) + end, +} +-- }}} +-- Diagnostic Saving & Caching {{{ +local _diagnostic_cleanup = setmetatable({}, bufnr_and_client_cacher_mt) +local diagnostic_cache = setmetatable({}, bufnr_and_client_cacher_mt) +local diagnostic_cache_lines = setmetatable({}, bufnr_and_client_cacher_mt) +local diagnostic_cache_counts = setmetatable({}, bufnr_and_client_cacher_mt) + +local _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt) + +--- Store Diagnostic[] by line +--- +---@param diagnostics Diagnostic[] +---@return table<number, Diagnostic[]> +local _diagnostic_lines = function(diagnostics) + if not diagnostics then return end + + local diagnostics_by_line = {} + for _, diagnostic in ipairs(diagnostics) do + local start = diagnostic.range.start + local line_diagnostics = diagnostics_by_line[start.line] + if not line_diagnostics then + line_diagnostics = {} + diagnostics_by_line[start.line] = line_diagnostics + end + table.insert(line_diagnostics, diagnostic) + end + return diagnostics_by_line +end + +--- Get the count of M by Severity +--- +---@param diagnostics Diagnostic[] +---@return table<DiagnosticSeverity, number> +local _diagnostic_counts = function(diagnostics) + if not diagnostics then return end + + local counts = {} + for _, diagnostic in pairs(diagnostics) do + if diagnostic.severity then + local val = counts[diagnostic.severity] + if val == nil then + val = 0 + end + + counts[diagnostic.severity] = val + 1 + end + end + + return counts +end + +--@private +--- Set the different diagnostic cache after `textDocument/publishDiagnostics` +---@param diagnostics Diagnostic[] +---@param bufnr number +---@param client_id number +---@return nil +local function set_diagnostic_cache(diagnostics, bufnr, client_id) + client_id = get_client_id(client_id) + + -- 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. + -- TODO: Replace this with server-specific heuristics to infer severity. + for _, diagnostic in ipairs(diagnostics) do + if diagnostic.severity == nil then + diagnostic.severity = DiagnosticSeverity.Error + end + end + + diagnostic_cache[bufnr][client_id] = diagnostics + diagnostic_cache_lines[bufnr][client_id] = _diagnostic_lines(diagnostics) + diagnostic_cache_counts[bufnr][client_id] = _diagnostic_counts(diagnostics) +end + + +--@private +--- Clear the cached diagnostics +---@param bufnr number +---@param client_id number +local function clear_diagnostic_cache(bufnr, client_id) + client_id = get_client_id(client_id) + + diagnostic_cache[bufnr][client_id] = nil + diagnostic_cache_lines[bufnr][client_id] = nil + diagnostic_cache_counts[bufnr][client_id] = nil +end + +--- Save diagnostics to the current buffer. +--- +--- Handles saving diagnostics from multiple clients in the same buffer. +---@param diagnostics Diagnostic[] +---@param bufnr number +---@param client_id number +function M.save(diagnostics, bufnr, client_id) + validate { + diagnostics = {diagnostics, 't'}, + bufnr = {bufnr, 'n'}, + client_id = {client_id, 'n', true}, + } + + if not diagnostics then return end + + bufnr = get_bufnr(bufnr) + client_id = get_client_id(client_id) + + if not _diagnostic_cleanup[bufnr][client_id] then + _diagnostic_cleanup[bufnr][client_id] = true + + -- Clean up our data when the buffer unloads. + api.nvim_buf_attach(bufnr, false, { + on_detach = function(b) + clear_diagnostic_cache(b, client_id) + _diagnostic_cleanup[bufnr][client_id] = nil + end + }) + end + + set_diagnostic_cache(diagnostics, bufnr, client_id) +end +-- }}} +-- Diagnostic Retrieval {{{ + +--- Return associated diagnostics for bufnr +--- +---@param bufnr number +---@param client_id number|nil If nil, then return all of the diagnostics. +--- Else, return just the diagnostics associated with the client_id. +function M.get(bufnr, client_id) + if client_id == nil then + local all_diagnostics = {} + for iter_client_id, _ in pairs(diagnostic_cache[bufnr]) do + local iter_diagnostics = M.get(bufnr, iter_client_id) + + for _, diagnostic in ipairs(iter_diagnostics) do + table.insert(all_diagnostics, diagnostic) + end + end + + return all_diagnostics + end + + return diagnostic_cache[bufnr][client_id] or {} +end + +--- Get the diagnostics by line +--- +---@param bufnr number The buffer number +---@param line_nr number The line number +---@param opts table|nil Configuration keys +--- - severity: (DiagnosticSeverity, default nil) +--- - Only return diagnostics with this severity. Overrides severity_limit +--- - severity_limit: (DiagnosticSeverity, default nil) +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +---@param client_id number the client id +---@return table Table with map of line number to list of diagnostics. +-- Structured: { [1] = {...}, [5] = {.... } } +function M.get_line_diagnostics(bufnr, line_nr, opts, client_id) + opts = opts or {} + + bufnr = bufnr or vim.api.nvim_get_current_buf() + line_nr = line_nr or vim.api.nvim_win_get_cursor(0)[1] - 1 + + local client_get_diags = function(iter_client_id) + return (diagnostic_cache_lines[bufnr][iter_client_id] or {})[line_nr] or {} + end + + local line_diagnostics + if client_id == nil then + line_diagnostics = {} + for iter_client_id, _ in pairs(diagnostic_cache_lines[bufnr]) do + for _, diagnostic in ipairs(client_get_diags(iter_client_id)) do + table.insert(line_diagnostics, diagnostic) + end + end + else + line_diagnostics = vim.deepcopy(client_get_diags(client_id)) + end + + if opts.severity then + local filter_level = to_severity(opts.severity) + line_diagnostics = vim.tbl_filter(function(t) return t.severity == filter_level end, line_diagnostics) + elseif opts.severity_limit then + local filter_level = to_severity(opts.severity_limit) + line_diagnostics = vim.tbl_filter(function(t) return t.severity <= filter_level end, line_diagnostics) + end + + if opts.severity_sort then + table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end) + end + + return line_diagnostics +end + +--- Get the counts for a particular severity +--- +--- Useful for showing diagnostic counts in statusline. eg: +--- +--- <pre> +--- function! LspStatus() abort +--- let sl = '' +--- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))') +--- let sl.='%#MyStatuslineLSP#E:' +--- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.diagnostic.get_count([[Error]])")}' +--- let sl.='%#MyStatuslineLSP# W:' +--- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.diagnostic.get_count([[Warning]])")}' +--- else +--- let sl.='%#MyStatuslineLSPErrors#off' +--- endif +--- return sl +--- endfunction +--- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus() +--- </pre> +--- +---@param bufnr number The buffer number +---@param severity DiagnosticSeverity +---@param client_id number the client id +function M.get_count(bufnr, severity, client_id) + if client_id == nil then + local total = 0 + for iter_client_id, _ in pairs(diagnostic_cache_counts[bufnr]) do + total = total + M.get_count(bufnr, severity, iter_client_id) + end + + return total + end + + return (diagnostic_cache_counts[bufnr][client_id] or {})[DiagnosticSeverity[severity]] or 0 +end + + +-- }}} +-- Diagnostic Movements {{{ + +--- Helper function to iterate through all of the diagnostic lines +---@return table list of diagnostics +local _iter_diagnostic_lines = function(start, finish, step, bufnr, opts, client_id) + if bufnr == nil then + bufnr = vim.api.nvim_get_current_buf() + end + + local wrap = if_nil(opts.wrap, true) + + local search = function(search_start, search_finish, search_step) + for line_nr = search_start, search_finish, search_step do + local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id) + if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then + return line_diagnostics + end + end + end + + local result = search(start, finish, step) + + if wrap then + local wrap_start, wrap_finish + if step == 1 then + wrap_start, wrap_finish = 1, start + else + wrap_start, wrap_finish = vim.api.nvim_buf_line_count(bufnr), start + end + + if not result then + result = search(wrap_start, wrap_finish, step) + end + end + + return result +end + +--@private +--- Helper function to ierate through diagnostic lines and return a position +--- +---@return table {row, col} +local function _iter_diagnostic_lines_pos(opts, line_diagnostics) + opts = opts or {} + + local win_id = opts.win_id or vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(win_id) + + if line_diagnostics == nil or vim.tbl_isempty(line_diagnostics) then + return false + end + + local iter_diagnostic = line_diagnostics[1] + return to_position(iter_diagnostic.range.start, bufnr) +end + +--@private +-- Move to the diagnostic position +local function _iter_diagnostic_move_pos(name, opts, pos) + opts = opts or {} + + local enable_popup = if_nil(opts.enable_popup, true) + local win_id = opts.win_id or vim.api.nvim_get_current_win() + + if not pos then + print(string.format("%s: No more valid diagnostics to move to.", name)) + return + end + + vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]}) + + if enable_popup then + -- This is a bit weird... I'm surprised that we need to wait til the next tick to do this. + vim.schedule(function() + M.show_line_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id)) + end) + end +end + +--- Get the previous diagnostic closest to the cursor_position +--- +---@param opts table See |vim.lsp.diagnostics.goto_next()| +---@return table Previous diagnostic +function M.get_prev(opts) + opts = opts or {} + + local win_id = opts.win_id or vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(win_id) + local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id) + + return _iter_diagnostic_lines(cursor_position[1] - 2, 0, -1, bufnr, opts, opts.client_id) +end + +--- Return the pos, {row, col}, for the prev diagnostic in the current buffer. +---@param opts table See |vim.lsp.diagnostics.goto_next()| +---@return table Previous diagnostic position +function M.get_prev_pos(opts) + return _iter_diagnostic_lines_pos( + opts, + M.get_prev(opts) + ) +end + +--- Move to the previous diagnostic +---@param opts table See |vim.lsp.diagnostics.goto_next()| +function M.goto_prev(opts) + return _iter_diagnostic_move_pos( + "DiagnosticPrevious", + opts, + M.get_prev_pos(opts) + ) +end + +--- Get the previous diagnostic closest to the cursor_position +---@param opts table See |vim.lsp.diagnostics.goto_next()| +---@return table Next diagnostic +function M.get_next(opts) + opts = opts or {} + + local win_id = opts.win_id or vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(win_id) + local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id) + + return _iter_diagnostic_lines(cursor_position[1], vim.api.nvim_buf_line_count(bufnr), 1, bufnr, opts, opts.client_id) +end + +--- Return the pos, {row, col}, for the next diagnostic in the current buffer. +---@param opts table See |vim.lsp.diagnostics.goto_next()| +---@return table Next diagnostic position +function M.get_next_pos(opts) + return _iter_diagnostic_lines_pos( + opts, + M.get_next(opts) + ) +end + +--- Move to the next diagnostic +---@param opts table|nil Configuration table. Keys: +--- - {client_id}: (number) +--- - If nil, will consider all clients attached to buffer. +--- - {cursor_position}: (Position, default current position) +--- - See |nvim_win_get_cursor()| +--- - {wrap}: (boolean, default true) +--- - Whether to loop around file or not. Similar to 'wrapscan' +--- - {severity}: (DiagnosticSeverity) +--- - Exclusive severity to consider. Overrides {severity_limit} +--- - {severity_limit}: (DiagnosticSeverity) +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +--- - {enable_popup}: (boolean, default true) +--- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump +--- - {popup_opts}: (table) +--- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()| +--- - {win_id}: (number, default 0) +--- - Window ID +function M.goto_next(opts) + return _iter_diagnostic_move_pos( + "DiagnosticNext", + opts, + M.get_next_pos(opts) + ) +end +-- }}} +-- Diagnostic Setters {{{ + +--- Set signs for given diagnostics +--- +--- Sign characters can be customized with the following commands: +--- +--- <pre> +--- sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl= +--- sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl= +--- sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl= +--- sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl= +--- </pre> +---@param diagnostics Diagnostic[] +---@param bufnr number The buffer number +---@param client_id number the client id +---@param sign_ns number|nil +---@param opts table Configuration for signs. Keys: +--- - priority: Set the priority of the signs. +function M.set_signs(diagnostics, bufnr, client_id, sign_ns, opts) + opts = opts or {} + sign_ns = sign_ns or M._get_sign_namespace(client_id) + + if not diagnostics then + diagnostics = diagnostic_cache[bufnr][client_id] + end + + if not diagnostics then + return + end + + bufnr = get_bufnr(bufnr) + + local ok = true + for _, diagnostic in ipairs(diagnostics) do + ok = ok and pcall(vim.fn.sign_place, + 0, + sign_ns, + sign_highlight_map[diagnostic.severity], + bufnr, + { + priority = opts.priority, + lnum = diagnostic.range.start.line + 1 + } + ) + end + + if not ok then + log.debug("Failed to place signs:", diagnostics) + end +end + +--- Set underline for given diagnostics +--- +--- Underline highlights can be customized by changing the following |:highlight| groups. +--- +--- <pre> +--- LspDiagnosticsUnderlineError +--- LspDiagnosticsUnderlineWarning +--- LspDiagnosticsUnderlineInformation +--- LspDiagnosticsUnderlineHint +--- </pre> +--- +---@param diagnostics Diagnostic[] +---@param bufnr number The buffer number +---@param client_id number the client id +---@param diagnostic_ns number|nil +---@param opts table Currently unused. +function M.set_underline(diagnostics, bufnr, client_id, diagnostic_ns, opts) + opts = opts or {} + assert(opts) -- lint + + diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id) + + for _, diagnostic in ipairs(diagnostics) do + local start = diagnostic.range["start"] + local finish = diagnostic.range["end"] + local higroup = underline_highlight_map[diagnostic.severity] + + if higroup == nil then + -- Default to error if we don't have a highlight associated + higroup = underline_highlight_map[DiagnosticSeverity.Error] + end + + highlight.range( + bufnr, + diagnostic_ns, + higroup, + to_position(start, bufnr), + to_position(finish, bufnr) + ) + end +end + +-- Virtual Text {{{ +--- Set virtual text given diagnostics +--- +--- Virtual text highlights can be customized by changing the following |:highlight| groups. +--- +--- <pre> +--- LspDiagnosticsVirtualTextError +--- LspDiagnosticsVirtualTextWarning +--- LspDiagnosticsVirtualTextInformation +--- LspDiagnosticsVirtualTextHint +--- </pre> +--- +---@param diagnostics Diagnostic[] +---@param bufnr number +---@param client_id number +---@param diagnostic_ns number +---@param opts table Options on how to display virtual text. Keys: +--- - prefix (string): Prefix to display before virtual text on line +--- - spacing (number): Number of spaces to insert before virtual text +function M.set_virtual_text(diagnostics, bufnr, client_id, diagnostic_ns, opts) + opts = opts or {} + + client_id = get_client_id(client_id) + diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id) + + local buffer_line_diagnostics + if diagnostics then + buffer_line_diagnostics = _diagnostic_lines(diagnostics) + else + buffer_line_diagnostics = diagnostic_cache_lines[bufnr][client_id] + end + + if not buffer_line_diagnostics then + return nil + end + + for line, line_diagnostics in pairs(buffer_line_diagnostics) do + local virt_texts = M.get_virtual_text_chunks_for_line(bufnr, line, line_diagnostics, opts) + + if virt_texts then + api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {}) + end + end +end + +--- Default function to get text chunks to display using `nvim_buf_set_virtual_text`. +---@param bufnr number The buffer to display the virtual text in +---@param line number The line number to display the virtual text on +---@param line_diags Diagnostic[] The diagnostics associated with the line +---@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()| +---@return table chunks, as defined by |nvim_buf_set_virtual_text()| +function M.get_virtual_text_chunks_for_line(bufnr, line, line_diags, opts) + assert(bufnr or line) + + if #line_diags == 0 then + return nil + end + + opts = opts or {} + local prefix = opts.prefix or "■" + local spacing = opts.spacing or 4 + + -- Create a little more space between virtual text and contents + local virt_texts = {{string.rep(" ", spacing)}} + + for i = 1, #line_diags - 1 do + table.insert(virt_texts, {prefix, virtual_text_highlight_map[line_diags[i].severity]}) + end + local last = line_diags[#line_diags] + + -- TODO(tjdevries): Allow different servers to be shown first somehow? + -- TODO(tjdevries): Display server name associated with these? + if last.message then + table.insert( + virt_texts, + { + string.format("%s %s", prefix, last.message:gsub("\r", ""):gsub("\n", " ")), + virtual_text_highlight_map[last.severity] + } + ) + + return virt_texts + end +end +-- }}} +-- }}} +-- Diagnostic Clear {{{ +--- Clears the currently displayed diagnostics +---@param bufnr number The buffer number +---@param client_id number the client id +---@param diagnostic_ns number|nil Associated diagnostic namespace +---@param sign_ns number|nil Associated sign namespace +function M.clear(bufnr, client_id, diagnostic_ns, sign_ns) + validate { bufnr = { bufnr, 'n' } } + + bufnr = (bufnr == 0 and api.nvim_get_current_buf()) or bufnr + + if client_id == nil then + return vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _) + return M.clear(bufnr, iter_client_id) + end) + end + + diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id) + sign_ns = sign_ns or M._get_sign_namespace(client_id) + + assert(bufnr, "bufnr is required") + assert(diagnostic_ns, "Need diagnostic_ns, got nil") + assert(sign_ns, string.format("Need sign_ns, got nil %s", sign_ns)) + + -- clear sign group + vim.fn.sign_unplace(sign_ns, {buffer=bufnr}) + + -- clear virtual text namespace + api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) +end +-- }}} +-- Diagnostic Insert Leave Handler {{{ + +--- Callback scheduled for after leaving insert mode +--- +--- Used to handle +--@private +function M._execute_scheduled_display(bufnr, client_id) + local args = _bufs_waiting_to_update[bufnr][client_id] + if not args then + return + end + + -- Clear the args so we don't display unnecessarily. + _bufs_waiting_to_update[bufnr][client_id] = nil + + M.display(nil, bufnr, client_id, args) +end + +local registered = {} + +local make_augroup_key = function(bufnr, client_id) + return string.format("LspDiagnosticInsertLeave:%s:%s", bufnr, client_id) +end + +--- Table of autocmd events to fire the update for displaying new diagnostic information +M.insert_leave_auto_cmds = { "InsertLeave", "CursorHoldI" } + +--- Used to schedule diagnostic updates upon leaving insert mode. +--- +--- For parameter description, see |M.display()| +function M._schedule_display(bufnr, client_id, args) + _bufs_waiting_to_update[bufnr][client_id] = args + + local key = make_augroup_key(bufnr, client_id) + if not registered[key] then + vim.cmd(string.format("augroup %s", key)) + vim.cmd(" au!") + vim.cmd( + string.format( + [[autocmd %s <buffer=%s> :lua vim.lsp.diagnostic._execute_scheduled_display(%s, %s)]], + table.concat(M.insert_leave_auto_cmds, ","), + bufnr, + bufnr, + client_id + ) + ) + vim.cmd("augroup END") + + registered[key] = true + end +end + + +--- Used in tandem with +--- +--- For parameter description, see |M.display()| +function M._clear_scheduled_display(bufnr, client_id) + local key = make_augroup_key(bufnr, client_id) + + if registered[key] then + vim.cmd(string.format("augroup %s", key)) + vim.cmd(" au!") + vim.cmd("augroup END") + + registered[key] = nil + end +end +-- }}} + +-- Diagnostic Private Highlight Utilies {{{ +--- Get the severity highlight name +--@private +function M._get_severity_highlight_name(severity) + return virtual_text_highlight_map[severity] +end + +--- Get floating severity highlight name +--@private +function M._get_floating_severity_highlight_name(severity) + return floating_highlight_map[severity] +end + +--- This should be called to update the highlights for the LSP client. +function M._define_default_signs_and_highlights() + --@private + local function define_default_sign(name, properties) + if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then + vim.fn.sign_define(name, properties) + end + end + + -- Initialize default diagnostic highlights + for severity, hi_info in pairs(diagnostic_severities) do + local default_highlight_name = default_highlight_map[severity] + highlight.create(default_highlight_name, hi_info, true) + + -- Default link all corresponding highlights to the default highlight + highlight.link(virtual_text_highlight_map[severity], default_highlight_name, false) + highlight.link(floating_highlight_map[severity], default_highlight_name, false) + highlight.link(sign_highlight_map[severity], default_highlight_name, false) + end + + -- Create all signs + for severity, sign_hl_name in pairs(sign_highlight_map) do + local severity_name = DiagnosticSeverity[severity] + + define_default_sign(sign_hl_name, { + text = (severity_name or 'U'):sub(1, 1), + texthl = sign_hl_name, + linehl = '', + numhl = '', + }) + end + + -- Initialize Underline highlights + for severity, underline_highlight_name in pairs(underline_highlight_map) do + highlight.create(underline_highlight_name, { + cterm = 'underline', + gui = 'underline', + guisp = diagnostic_severities[severity].guifg + }, true) + end +end +-- }}} +-- Diagnostic Display {{{ + +--- |lsp-handler| for the method "textDocument/publishDiagnostics" +--- +---@note Each of the configuration options accepts: +--- - `false`: Disable this feature +--- - `true`: Enable this feature, use default settings. +--- - `table`: Enable this feature, use overrides. +--- - `function`: Function with signature (bufnr, client_id) that returns any of the above. +--- <pre> +--- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( +--- vim.lsp.diagnostic.on_publish_diagnostics, { +--- -- Enable underline, use default values +--- underline = true, +--- -- Enable virtual text, override spacing to 4 +--- virtual_text = { +--- spacing = 4, +--- }, +--- -- Use a function to dynamically turn signs off +--- -- and on, using buffer local variables +--- signs = function(bufnr, client_id) +--- return vim.bo[bufnr].show_signs == false +--- end, +--- -- Disable a feature +--- update_in_insert = false, +--- } +--- ) +--- </pre> +--- +---@param config table Configuration table. +--- - underline: (default=true) +--- - Apply underlines to diagnostics. +--- - See |vim.lsp.diagnostic.set_underline()| +--- - virtual_text: (default=true) +--- - Apply virtual text to line endings. +--- - See |vim.lsp.diagnostic.set_virtual_text()| +--- - signs: (default=true) +--- - Apply signs for diagnostics. +--- - See |vim.lsp.diagnostic.set_signs()| +--- - update_in_insert: (default=false) +--- - Update diagnostics in InsertMode or wait until InsertLeave +function M.on_publish_diagnostics(_, _, params, client_id, _, config) + local uri = params.uri + local bufnr = vim.uri_to_bufnr(uri) + + if not bufnr then + return + end + + local diagnostics = params.diagnostics + + -- 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. + M.save(diagnostics, bufnr, client_id) + + -- 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 + + M.display(diagnostics, bufnr, client_id, config) +end + +--@private +--- Display diagnostics for the buffer, given a configuration. +function M.display(diagnostics, bufnr, client_id, config) + config = vim.lsp._with_extend('vim.lsp.diagnostic.on_publish_diagnostics', { + signs = true, + underline = true, + virtual_text = true, + update_in_insert = false, + }, config) + + if diagnostics == nil then + diagnostics = M.get(bufnr, client_id) + end + + -- TODO(tjdevries): Consider how we can make this a "standardized" kind of thing for |lsp-handlers|. + -- It seems like we would probably want to do this more often as we expose more of them. + -- It provides a very nice functional interface for people to override configuration. + local resolve_optional_value = function(option) + local enabled_val = {} + + if not option then + return false + elseif option == true then + return enabled_val + elseif type(option) == 'function' then + local val = option(bufnr, client_id) + if val == true then + return enabled_val + else + return val + end + elseif type(option) == 'table' then + return option + else + error("Unexpected option type: " .. vim.inspect(option)) + end + end + + if resolve_optional_value(config.update_in_insert) then + M._clear_scheduled_display(bufnr, client_id) + else + local mode = vim.api.nvim_get_mode() + + if string.sub(mode.mode, 1, 1) == 'i' then + M._schedule_display(bufnr, client_id, config) + return + end + end + + M.clear(bufnr, client_id) + + diagnostics = diagnostics or diagnostic_cache[bufnr][client_id] + + if not diagnostics or vim.tbl_isempty(diagnostics) then + return + end + + local underline_opts = resolve_optional_value(config.underline) + if underline_opts then + M.set_underline(diagnostics, bufnr, client_id, nil, underline_opts) + end + + local virtual_text_opts = resolve_optional_value(config.virtual_text) + if virtual_text_opts then + M.set_virtual_text(diagnostics, bufnr, client_id, nil, virtual_text_opts) + end + + local signs_opts = resolve_optional_value(config.signs) + if signs_opts then + M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts) + end + + vim.api.nvim_command("doautocmd User LspDiagnosticsChanged") +end +-- }}} +-- Diagnostic User Functions {{{ + +--- Open a floating window with the diagnostics from {line_nr} +--- +--- The floating window can be customized with the following highlight groups: +--- <pre> +--- LspDiagnosticsFloatingError +--- LspDiagnosticsFloatingWarning +--- LspDiagnosticsFloatingInformation +--- LspDiagnosticsFloatingHint +--- </pre> +---@param opts table Configuration table +--- - show_header (boolean, default true): Show "Diagnostics:" header. +---@param bufnr number The buffer number +---@param line_nr number The line number +---@param client_id number|nil the client id +---@return {popup_bufnr, win_id} +function M.show_line_diagnostics(opts, bufnr, line_nr, client_id) + opts = opts or {} + opts.severity_sort = if_nil(opts.severity_sort, true) + + local show_header = if_nil(opts.show_header, true) + + bufnr = bufnr or 0 + line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1) + + local lines = {} + local highlights = {} + if show_header then + table.insert(lines, "Diagnostics:") + table.insert(highlights, {0, "Bold"}) + end + + local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id) + if vim.tbl_isempty(line_diagnostics) then return end + + for i, diagnostic in ipairs(line_diagnostics) do + local prefix = string.format("%d. ", i) + local hiname = M._get_floating_severity_highlight_name(diagnostic.severity) + assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) + + local message_lines = vim.split(diagnostic.message, '\n', true) + table.insert(lines, prefix..message_lines[1]) + table.insert(highlights, {#prefix + 1, hiname}) + for j = 2, #message_lines do + table.insert(lines, message_lines[j]) + table.insert(highlights, {0, hiname}) + end + end + + local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext') + for i, hi in ipairs(highlights) do + local prefixlen, hiname = unpack(hi) + -- Start highlight after the prefix + api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1) + end + + return popup_bufnr, winnr +end + +local loclist_type_map = { + [DiagnosticSeverity.Error] = 'E', + [DiagnosticSeverity.Warning] = 'W', + [DiagnosticSeverity.Information] = 'I', + [DiagnosticSeverity.Hint] = 'I', +} + +--- Sets the location list +---@param opts table|nil Configuration table. Keys: +--- - {open_loclist}: (boolean, default true) +--- - Open loclist after set +--- - {client_id}: (number) +--- - If nil, will consider all clients attached to buffer. +--- - {severity}: (DiagnosticSeverity) +--- - Exclusive severity to consider. Overrides {severity_limit} +--- - {severity_limit}: (DiagnosticSeverity) +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +function M.set_loclist(opts) + opts = opts or {} + + local open_loclist = if_nil(opts.open_loclist, true) + + local bufnr = vim.api.nvim_get_current_buf() + local buffer_diags = M.get(bufnr, opts.client_id) + + local severity = to_severity(opts.severity) + local severity_limit = to_severity(opts.severity_limit) + + local items = {} + local insert_diag = function(diag) + if severity then + -- Handle missing severities + if not diag.severity then + return + end + + if severity ~= diag.severity then + return + end + elseif severity_limit then + if not diag.severity then + return + end + + if severity_limit < diag.severity then + return + end + end + + local pos = diag.range.start + local row = pos.line + local col = util.character_offset(bufnr, row, pos.character) + + local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1] + + table.insert(items, { + bufnr = bufnr, + lnum = row + 1, + col = col + 1, + text = line .. " | " .. diag.message, + type = loclist_type_map[diag.severity or DiagnosticSeverity.Error] or 'E', + }) + end + + for _, diag in ipairs(buffer_diags) do + insert_diag(diag) + end + + table.sort(items, function(a, b) return a.lnum < b.lnum end) + + util.set_loclist(items) + if open_loclist then + vim.cmd [[lopen]] + end +end +-- }}} + +return M diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua new file mode 100644 index 0000000000..e034923afb --- /dev/null +++ b/runtime/lua/vim/lsp/handlers.lua @@ -0,0 +1,310 @@ +local log = require 'vim.lsp.log' +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 = {} + +-- FIXME: DOC: Expose in vimdocs + +--@private +--- Writes to error buffer. +--@param ... (table of strings) Will be concatenated before being written +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand +M['workspace/executeCommand'] = function(err, _) + if err then + error("Could not execute code action: "..err.message) + end +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction +M['textDocument/codeAction'] = function(_, _, actions) + if actions == nil or vim.tbl_isempty(actions) then + print("No code actions available") + return + end + + local option_strings = {"Code Actions:"} + for i, action in ipairs(actions) do + local title = action.title:gsub('\r\n', '\\r\\n') + title = title:gsub('\n', '\\n') + table.insert(option_strings, string.format("%d. %s", i, title)) + end + + local choice = vim.fn.inputlist(option_strings) + if choice < 1 or choice > #actions then + return + end + local action_chosen = actions[choice] + -- textDocument/codeAction can return either Command[] or CodeAction[]. + -- If it is a CodeAction, it can have either an edit, a command or both. + -- Edits should be executed first + if action_chosen.edit or type(action_chosen.command) == "table" then + if action_chosen.edit then + util.apply_workspace_edit(action_chosen.edit) + end + if type(action_chosen.command) == "table" then + buf.execute_command(action_chosen.command) + end + else + buf.execute_command(action_chosen) + end +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 + -- TODO(ashkan) Do something more with label? + if workspace_edit.label then + print("Workspace edit", workspace_edit.label) + end + local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit) + return { + applied = status; + failureReason = result; + } +end + +M['textDocument/publishDiagnostics'] = function(...) + return require('vim.lsp.diagnostic').on_publish_diagnostics(...) +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references +M['textDocument/references'] = function(_, _, result) + if not result then return end + util.set_qflist(util.locations_to_items(result)) + api.nvim_command("copen") + api.nvim_command("wincmd p") +end + +--@private +--- Prints given list of symbols to the quickfix list. +--@param _ (not used) +--@param _ (not used) +--@param result (list of Symbols) LSP method name +--@param result (table) result of LSP method; a location or a list of locations. +---(`textDocument/definition` can return `Location` or `Location[]` +local symbol_handler = function(_, _, result, _, bufnr) + if not result or vim.tbl_isempty(result) then return end + + util.set_qflist(util.symbols_to_items(result, bufnr)) + api.nvim_command("copen") + api.nvim_command("wincmd p") +end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol +M['textDocument/documentSymbol'] = symbol_handler +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol +M['workspace/symbol'] = symbol_handler + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename +M['textDocument/rename'] = function(_, _, result) + if not result then return end + util.apply_workspace_edit(result) +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting +M['textDocument/rangeFormatting'] = function(_, _, result) + if not result then return end + util.apply_text_edits(result) +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting +M['textDocument/formatting'] = function(_, _, result) + if not result then return end + util.apply_text_edits(result) +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +M['textDocument/completion'] = function(_, _, result) + if vim.tbl_isempty(result or {}) then return end + local row, col = unpack(api.nvim_win_get_cursor(0)) + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) + local textMatch = vim.fn.match(line_to_cursor, '\\k*$') + local prefix = line_to_cursor:sub(textMatch+1) + + local matches = util.text_document_completion_list_to_complete_items(result, prefix) + vim.fn.complete(textMatch+1, matches) +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover +M['textDocument/hover'] = function(_, method, result) + util.focusable_float(method, function() + if not (result and result.contents) then + -- return { 'No information available' } + return + end + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + -- return { 'No information available' } + return + end + local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, { + pad_left = 1; pad_right = 1; + }) + util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr) + return bufnr, winnr + end) +end + +--@private +--- Jumps to a location. Used as a handler for multiple LSP methods. +--@param _ (not used) +--@param method (string) LSP method name +--@param result (table) result of LSP method; a location or a list of locations. +---(`textDocument/definition` can return `Location` or `Location[]` +local function location_handler(_, method, result) + if result == nil or vim.tbl_isempty(result) then + local _ = log.info() and log.info(method, 'No location found') + return nil + end + + -- textDocument/definition can return Location or Location[] + -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition + + if vim.tbl_islist(result) then + util.jump_to_location(result[1]) + + if #result > 1 then + util.set_qflist(util.locations_to_items(result)) + api.nvim_command("copen") + api.nvim_command("wincmd p") + end + else + util.jump_to_location(result) + end +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration +M['textDocument/declaration'] = location_handler +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition +M['textDocument/definition'] = location_handler +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition +M['textDocument/typeDefinition'] = location_handler +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation +M['textDocument/implementation'] = location_handler + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp +M['textDocument/signatureHelp'] = function(_, method, result) + -- When use `autocmd CompleteDone <silent><buffer> 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 `<silent>` to ignore + if not (result and result.signatures and result.signatures[1]) then + print('No signature help available') + return + end + local lines = util.convert_signature_help_to_markdown_lines(result) + lines = util.trim_empty_lines(lines) + if vim.tbl_isempty(lines) then + print('No signature help available') + return + end + util.focusable_preview(method, function() + return lines, util.try_trim_markdown_code_blocks(lines) + end) +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight +M['textDocument/documentHighlight'] = function(_, _, result, _) + if not result then return end + local bufnr = api.nvim_get_current_buf() + util.buf_highlight_references(bufnr, result) +end + +--@private +--- +--- Displays call hierarchy in the quickfix window. +--- +--@param direction `"from"` for incoming calls and `"to"` for outgoing calls +--@returns `CallHierarchyIncomingCall[]` if {direction} is `"from"`, +--@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`, +local make_call_hierarchy_handler = function(direction) + return function(_, _, result) + if not result then return end + local items = {} + for _, call_hierarchy_call in pairs(result) do + local call_hierarchy_item = call_hierarchy_call[direction] + for _, range in pairs(call_hierarchy_call.fromRanges) do + table.insert(items, { + filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)), + text = call_hierarchy_item.name, + lnum = range.start.line + 1, + col = range.start.character + 1, + }) + end + end + util.set_qflist(items) + api.nvim_command("copen") + api.nvim_command("wincmd p") + end +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/incomingCalls +M['callHierarchy/incomingCalls'] = make_call_hierarchy_handler('from') + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls +M['callHierarchy/outgoingCalls'] = make_call_hierarchy_handler('to') + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/logMessage +M['window/logMessage'] = function(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + err_message("LSP[", client_name, "] client has shut down after sending the message") + end + if message_type == protocol.MessageType.Error then + log.error(message) + elseif message_type == protocol.MessageType.Warning then + log.warn(message) + elseif message_type == protocol.MessageType.Info then + log.info(message) + else + log.debug(message) + end + return result +end + +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/showMessage +M['window/showMessage'] = function(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + err_message("LSP[", client_name, "] client has shut down after sending the message") + end + if message_type == protocol.MessageType.Error then + err_message("LSP[", client_name, "] ", message) + else + local message_type_name = protocol.MessageType[message_type] + api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) + end + return result +end + +-- Add boilerplate error validation and logging for all of these. +for k, fn in pairs(M) do + M[k] = function(err, method, params, client_id, bufnr, config) + local _ = log.debug() and log.debug('default_handler', method, { + params = params, client_id = client_id, err = err, bufnr = bufnr, config = config + }) + + if err then + error(tostring(err)) + end + + return fn(err, method, params, client_id, bufnr, config) + end +end + +return M +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 70862320c5..07b4e8b926 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -1,17 +1,8 @@ -- Protocol for the Microsoft Language Server Protocol (mslsp) -local protocol = {} - ---@private ---- Returns {a} if it is not nil, otherwise returns {b}. ---- ---@param a ---@param b -local function ifnil(a, b) - if a == nil then return b end - return a -end +local if_nil = vim.F.if_nil +local protocol = {} --[=[ --@private @@ -909,12 +900,12 @@ function protocol.resolve_capabilities(server_capabilities) } elseif type(textDocumentSync) == 'table' then text_document_sync_properties = { - text_document_open_close = ifnil(textDocumentSync.openClose, false); - text_document_did_change = ifnil(textDocumentSync.change, TextDocumentSyncKind.None); - text_document_will_save = ifnil(textDocumentSync.willSave, false); - text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false); - text_document_save = ifnil(textDocumentSync.save, false); - text_document_save_include_text = ifnil(type(textDocumentSync.save) == 'table' + text_document_open_close = if_nil(textDocumentSync.openClose, false); + text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None); + text_document_will_save = if_nil(textDocumentSync.willSave, false); + text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false); + text_document_save = if_nil(textDocumentSync.save, false); + text_document_save_include_text = if_nil(type(textDocumentSync.save) == 'table' and textDocumentSync.save.includeText, false); } else diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 17c411f952..bbcc8ea6f9 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -231,41 +231,42 @@ local function rpc_response_error(code, message, data) }) end -local default_handlers = {} +local default_dispatchers = {} + --@private ---- Default handler for notifications sent to an LSP server. +--- Default dispatcher for notifications sent to an LSP server. --- --@param method (string) The invoked LSP method --@param params (table): Parameters for the invoked LSP method -function default_handlers.notification(method, params) +function default_dispatchers.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) end --@private ---- Default handler for requests sent to an LSP server. +--- Default dispatcher for requests sent to an LSP server. --- --@param method (string) The invoked LSP method --@param params (table): Parameters for the invoked LSP method --@returns `nil` and `vim.lsp.protocol.ErrorCodes.MethodNotFound`. -function default_handlers.server_request(method, params) +function default_dispatchers.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound) end --@private ---- Default handler for when a client exits. +--- Default dispatcher for when a client exits. --- --@param code (number): Exit code --@param signal (number): Number describing the signal used to terminate (if ---any) -function default_handlers.on_exit(code, signal) +function default_dispatchers.on_exit(code, signal) local _ = log.info() and log.info("client_exit", { code = code, signal = signal }) end --@private ---- Default handler for client errors. +--- Default dispatcher for client errors. --- --@param code (number): Error code --@param err (any): Details about the error ---any) -function default_handlers.on_error(code, err) +function default_dispatchers.on_error(code, err) local _ = log.error() and log.error('client_error:', client_errors[code], err) end @@ -274,8 +275,8 @@ end --- --@param cmd (string) Command to start the LSP server. --@param cmd_args (table) List of additional string arguments to pass to {cmd}. ---@param handlers (table, optional) Handlers for LSP message types. Valid ----handler names are: +--@param dispatchers (table, optional) Dispatchers for LSP message types. Valid +---dispatcher names are: --- - `"notification"` --- - `"server_request"` --- - `"on_error"` @@ -294,39 +295,39 @@ end --- - {pid} (number) The LSP server's PID. --- - {handle} A handle for low-level interaction with the LSP server process --- |vim.loop|. -local function start(cmd, cmd_args, handlers, extra_spawn_params) +local function start(cmd, cmd_args, dispatchers, extra_spawn_params) local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params}) validate { cmd = { cmd, 's' }; cmd_args = { cmd_args, 't' }; - handlers = { handlers, 't', true }; + dispatchers = { dispatchers, 't', true }; } if not (vim.fn.executable(cmd) == 1) then error(string.format("The given command %q is not executable.", cmd)) end - if handlers then - local user_handlers = handlers - handlers = {} - for handle_name, default_handler in pairs(default_handlers) do - local user_handler = user_handlers[handle_name] - if user_handler then - if type(user_handler) ~= 'function' then - error(string.format("handler.%s must be a function", handle_name)) + if dispatchers then + local user_dispatchers = dispatchers + dispatchers = {} + for dispatch_name, default_dispatch in pairs(default_dispatchers) do + local user_dispatcher = user_dispatchers[dispatch_name] + if user_dispatcher then + if type(user_dispatcher) ~= 'function' then + error(string.format("dispatcher.%s must be a function", dispatch_name)) end -- server_request is wrapped elsewhere. - if not (handle_name == 'server_request' - or handle_name == 'on_exit') -- TODO this blocks the loop exiting for some reason. + if not (dispatch_name == 'server_request' + or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason. then - user_handler = schedule_wrap(user_handler) + user_dispatcher = schedule_wrap(user_dispatcher) end - handlers[handle_name] = user_handler + dispatchers[dispatch_name] = user_dispatcher else - handlers[handle_name] = default_handler + dispatchers[dispatch_name] = default_dispatch end end else - handlers = default_handlers + dispatchers = default_dispatchers end local stdin = uv.new_pipe(false) @@ -339,8 +340,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params) local handle, pid do --@private - --- Callback for |vim.loop.spawn()| Closes all streams and runs the - --- `on_exit` handler. + --- Callback for |vim.loop.spawn()| Closes all streams and runs the `on_exit` dispatcher. --@param code (number) Exit code --@param signal (number) Signal that was used to terminate (if any) local function onexit(code, signal) @@ -350,7 +350,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params) handle:close() -- Make sure that message_callbacks can be gc'd. message_callbacks = nil - handlers.on_exit(code, signal) + dispatchers.on_exit(code, signal) end local spawn_params = { args = cmd_args; @@ -448,7 +448,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params) local function on_error(errkind, ...) assert(client_errors[errkind]) -- TODO what to do if this fails? - pcall(handlers.on_error, errkind, ...) + pcall(dispatchers.on_error, errkind, ...) end --@private local function pcall_handler(errkind, status, head, ...) @@ -471,7 +471,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params) local function handle_body(body) local decoded, err = json_decode(body) if not decoded then - on_error(client_errors.INVALID_SERVER_JSON, err) + -- on_error(client_errors.INVALID_SERVER_JSON, err) return end local _ = log.debug() and log.debug("decoded", decoded) @@ -484,7 +484,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params) schedule(function() local status, result status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR, - handlers.server_request, decoded.method, decoded.params) + dispatchers.server_request, decoded.method, decoded.params) local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err }) if status then if not (result or err) then @@ -551,7 +551,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params) -- Notification decoded.params = convert_NIL(decoded.params) try_call(client_errors.NOTIFICATION_HANDLER_ERROR, - handlers.notification, decoded.method, decoded.params) + dispatchers.notification, decoded.method, decoded.params) else -- Invalid server message on_error(client_errors.INVALID_SERVER_MESSAGE, decoded) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 9ed19b938d..3deec6d74e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -5,50 +5,32 @@ local api = vim.api local list_extend = vim.list_extend local highlight = require 'vim.highlight' +local npcall = vim.F.npcall +local split = vim.split + +local _warned = {} +local warn_once = function(message) + if not _warned[message] then + vim.api.nvim_err_writeln(message) + _warned[message] = true + end +end + local M = {} --- FIXME: DOC: Expose in vimdocs ---- Diagnostics received from the server via `textDocument/publishDiagnostics` --- by buffer. --- --- {<bufnr>: {diagnostics}} --- --- This contains only entries for active buffers. Entries for detached buffers --- are discarded. --- --- If you override the `textDocument/publishDiagnostic` callback, --- this will be empty unless you call `buf_diagnostics_save_positions`. --- --- --- Diagnostic is: --- --- { --- range: Range --- message: string --- severity?: DiagnosticSeverity --- code?: number | string --- source?: string --- tags?: DiagnosticTag[] --- relatedInformation?: DiagnosticRelatedInformation[] --- } -M.diagnostics_by_buf = {} +-- TODO(remove-callbacks) +M.diagnostics_by_buf = setmetatable({}, { + __index = function(_, bufnr) + warn_once("diagnostics_by_buf is deprecated. Use 'vim.lsp.diagnostic.get'") + return vim.lsp.diagnostic.get(bufnr) + end +}) -local split = vim.split --@private local function split_lines(value) return split(value, '\n', true) end ---@private -local function ok_or_nil(status, ...) - if not status then return end - return ... -end ---@private -local function npcall(fn, ...) - return ok_or_nil(pcall(fn, ...)) -end - --- Replaces text in a range with new text. --- --- CAUTION: Changes in-place! @@ -121,10 +103,18 @@ local function get_line_byte_from_position(bufnr, position) -- When on the first character, we can ignore the difference between byte and -- character if col > 0 then + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end + local line = position.line local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false) if #lines > 0 then - return vim.str_byteindex(lines[1], col) + local ok, result = pcall(vim.str_byteindex, lines[1], col) + + if ok then + return result + end end end return col @@ -700,13 +690,13 @@ end --- Trims empty lines from input and pad left and right with spaces --- ---@param contents table of lines to trim and pad ---@param opts dictionary with optional fields --- - pad_left number of columns to pad contents at left (default 1) --- - pad_right number of columns to pad contents at right (default 1) --- - pad_top number of lines to pad contents at top (default 0) --- - pad_bottom number of lines to pad contents at bottom (default 0) ---@returns contents table of trimmed and padded lines +---@param contents table of lines to trim and pad +---@param opts dictionary with optional fields +--- - pad_left number of columns to pad contents at left (default 1) +--- - pad_right number of columns to pad contents at right (default 1) +--- - pad_top number of lines to pad contents at top (default 0) +--- - pad_bottom number of lines to pad contents at bottom (default 0) +---@return contents table of trimmed and padded lines function M._trim_and_pad(contents, opts) validate { contents = { contents, 't' }; @@ -742,19 +732,19 @@ end --- regions to improve readability. --- The result is shown in a floating preview. --- ---@param contents table of lines to show in window ---@param opts dictionary with optional fields --- - height of floating window --- - width of floating window --- - wrap_at character to wrap at for computing height --- - max_width maximal width of floating window --- - max_height maximal height of floating window --- - pad_left number of columns to pad contents at left --- - pad_right number of columns to pad contents at right --- - pad_top number of lines to pad contents at top --- - pad_bottom number of lines to pad contents at bottom --- - separator insert separator after code block ---@returns width,height size of float +---@param contents table of lines to show in window +---@param opts dictionary with optional fields +--- - height of floating window +--- - width of floating window +--- - wrap_at character to wrap at for computing height +--- - max_width maximal width of floating window +--- - max_height maximal height of floating window +--- - pad_left number of columns to pad contents at left +--- - pad_right number of columns to pad contents at right +--- - pad_top number of lines to pad contents at top +--- - pad_bottom number of lines to pad contents at bottom +--- - separator insert separator after code block +---@returns width,height size of float function M.fancy_floating_markdown(contents, opts) validate { contents = { contents, 't' }; @@ -971,170 +961,80 @@ function M.open_floating_preview(contents, filetype, opts) return floating_bufnr, floating_winnr end +-- TODO(remove-callbacks) do - local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") - local reference_ns = api.nvim_create_namespace("vim_lsp_references") - local sign_ns = 'vim_lsp_signs' - local underline_highlight_name = "LspDiagnosticsUnderline" - vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) - for kind, _ in pairs(protocol.DiagnosticSeverity) do - if type(kind) == 'string' then - vim.cmd(string.format("highlight default link %s%s %s", underline_highlight_name, kind, underline_highlight_name)) - end + --@deprecated + function M.get_severity_highlight_name(severity) + warn_once("vim.lsp.util.get_severity_highlight_name is deprecated.") + return vim.lsp.diagnostic._get_severity_highlight_name(severity) end - local severity_highlights = {} + --@deprecated + function M.buf_clear_diagnostics(bufnr, client_id) + warn_once("buf_clear_diagnostics is deprecated. Use vim.lsp.diagnostic.clear") + return vim.lsp.diagnostic.clear(bufnr, client_id) + end - local severity_floating_highlights = {} + --@deprecated + function M.get_line_diagnostics() + warn_once("get_line_diagnostics is deprecated. Use vim.lsp.diagnostic.get_line_diagnostics") - local default_severity_highlight = { - [protocol.DiagnosticSeverity.Error] = { guifg = "Red" }; - [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" }; - [protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" }; - [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" }; - } + local bufnr = api.nvim_get_current_buf() + local line_nr = api.nvim_win_get_cursor(0)[1] - 1 - -- Initialize default severity highlights - for severity, hi_info in pairs(default_severity_highlight) do - local severity_name = protocol.DiagnosticSeverity[severity] - local highlight_name = "LspDiagnostics"..severity_name - local floating_highlight_name = highlight_name.."Floating" - -- Try to fill in the foreground color with a sane default. - local cmd_parts = {"highlight", "default", highlight_name} - for k, v in pairs(hi_info) do - table.insert(cmd_parts, k.."="..v) - end - api.nvim_command(table.concat(cmd_parts, ' ')) - api.nvim_command('highlight link ' .. highlight_name .. 'Sign ' .. highlight_name) - api.nvim_command('highlight link ' .. highlight_name .. 'Floating ' .. highlight_name) - severity_highlights[severity] = highlight_name - severity_floating_highlights[severity] = floating_highlight_name + return vim.lsp.diagnostic.get_line_diagnostics(bufnr, line_nr) end - --- Clears diagnostics for a buffer. - --- - --@param bufnr (number) buffer id - function M.buf_clear_diagnostics(bufnr) - validate { bufnr = {bufnr, 'n', true} } - bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr + --@deprecated + function M.show_line_diagnostics() + warn_once("show_line_diagnostics is deprecated. Use vim.lsp.diagnostic.show_line_diagnostics") - -- clear sign group - vim.fn.sign_unplace(sign_ns, {buffer=bufnr}) + local bufnr = api.nvim_get_current_buf() + local line_nr = api.nvim_win_get_cursor(0)[1] - 1 - -- clear virtual text namespace - api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) + return vim.lsp.diagnostic.show_line_diagnostics(bufnr, line_nr) end - --- Gets the name of a severity's highlight group. - --- - --@param severity A member of `vim.lsp.protocol.DiagnosticSeverity` - --@returns (string) Highlight group name - function M.get_severity_highlight_name(severity) - return severity_highlights[severity] + --@deprecated + function M.buf_diagnostics_save_positions(bufnr, diagnostics, client_id) + warn_once("buf_diagnostics_save_positions is deprecated. Use vim.lsp.diagnostic.save") + return vim.lsp.diagnostic.save(diagnostics, bufnr, client_id) end - --- Gets list of diagnostics for the current line. - --- - --@returns (table) list of `Diagnostic` tables - --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic - function M.get_line_diagnostics() - local bufnr = api.nvim_get_current_buf() - local linenr = api.nvim_win_get_cursor(0)[1] - 1 - - local buffer_diagnostics = M.diagnostics_by_buf[bufnr] + --@deprecated + function M.buf_diagnostics_get_positions(bufnr, client_id) + warn_once("buf_diagnostics_get_positions is deprecated. Use vim.lsp.diagnostic.get") + return vim.lsp.diagnostic.get(bufnr, client_id) + end - if not buffer_diagnostics then - return {} - end + --@deprecated + function M.buf_diagnostics_underline(bufnr, diagnostics, client_id) + warn_once("buf_diagnostics_underline is deprecated. Use 'vim.lsp.diagnostic.set_underline'") + return vim.lsp.diagnostic.set_underline(diagnostics, bufnr, client_id) + end - local diagnostics_by_line = M.diagnostics_group_by_line(buffer_diagnostics) - return diagnostics_by_line[linenr] or {} + --@deprecated + function M.buf_diagnostics_virtual_text(bufnr, diagnostics, client_id) + warn_once("buf_diagnostics_virtual_text is deprecated. Use 'vim.lsp.diagnostic.set_virtual_text'") + return vim.lsp.diagnostic.set_virtual_text(diagnostics, bufnr, client_id) end - --- Displays the diagnostics for the current line in a floating hover - --- window. - function M.show_line_diagnostics() - -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {}) - -- if #marks == 0 then - -- return - -- end - local lines = {"Diagnostics:"} - local highlights = {{0, "Bold"}} - local line_diagnostics = M.get_line_diagnostics() - if vim.tbl_isempty(line_diagnostics) then return end - - for i, diagnostic in ipairs(line_diagnostics) do - -- for i, mark in ipairs(marks) do - -- local mark_id = mark[1] - -- local diagnostic = buffer_diagnostics[mark_id] - - -- TODO(ashkan) make format configurable? - local prefix = string.format("%d. ", i) - local hiname = severity_floating_highlights[diagnostic.severity] - assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) - local message_lines = split_lines(diagnostic.message) - table.insert(lines, prefix..message_lines[1]) - table.insert(highlights, {#prefix + 1, hiname}) - for j = 2, #message_lines do - table.insert(lines, message_lines[j]) - table.insert(highlights, {0, hiname}) - end - end - local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext') - for i, hi in ipairs(highlights) do - local prefixlen, hiname = unpack(hi) - -- Start highlight after the prefix - api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1) - end - return popup_bufnr, winnr + --@deprecated + function M.buf_diagnostics_signs(bufnr, diagnostics, client_id) + warn_once("buf_diagnostics_signs is deprecated. Use 'vim.lsp.diagnostics.set_signs'") + return vim.lsp.diagnostic.set_signs(diagnostics, bufnr, client_id) end - --- Saves diagnostics into vim.lsp.util.diagnostics_by_buf[{bufnr}]. - --- - --@param bufnr (number) buffer id for which the diagnostics are for - --@param diagnostics list of `Diagnostic`s received from the LSP server - function M.buf_diagnostics_save_positions(bufnr, diagnostics) - validate { - bufnr = {bufnr, 'n', true}; - diagnostics = {diagnostics, 't', true}; - } - if not diagnostics then return end - bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr - - if not M.diagnostics_by_buf[bufnr] then - -- Clean up our data when the buffer unloads. - api.nvim_buf_attach(bufnr, false, { - on_detach = function(b) - M.diagnostics_by_buf[b] = nil - end - }) - end - M.diagnostics_by_buf[bufnr] = diagnostics + --@deprecated + function M.buf_diagnostics_count(kind, client_id) + warn_once("buf_diagnostics_count is deprecated. Use 'vim.lsp.diagnostic.get_count'") + return vim.lsp.diagnostic.get_count(vim.api.nvim_get_current_buf(), client_id, kind) end - --- Highlights a list of diagnostics in a buffer by underlining them. - --- - --@param bufnr (number) buffer id - --@param diagnostics (list of `Diagnostic`s) - function M.buf_diagnostics_underline(bufnr, diagnostics) - for _, diagnostic in ipairs(diagnostics) do - local start = diagnostic.range["start"] - local finish = diagnostic.range["end"] - - local hlmap = { - [protocol.DiagnosticSeverity.Error]='Error', - [protocol.DiagnosticSeverity.Warning]='Warning', - [protocol.DiagnosticSeverity.Information]='Information', - [protocol.DiagnosticSeverity.Hint]='Hint', - } +end - highlight.range(bufnr, diagnostic_ns, - underline_highlight_name..hlmap[diagnostic.severity], - {start.line, start.character}, - {finish.line, finish.character} - ) - end - end +do --[[ References ]] + local reference_ns = api.nvim_create_namespace("vim_lsp_references") --- Removes document highlights from a buffer. --- @@ -1162,109 +1062,6 @@ do highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos) end end - - --- Groups a list of diagnostics by line. - --- - --@param diagnostics (table) list of `Diagnostic`s - --@returns (table) dictionary mapping lines to lists of diagnostics valid on - ---those lines - --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic - function M.diagnostics_group_by_line(diagnostics) - if not diagnostics then return end - local diagnostics_by_line = {} - for _, diagnostic in ipairs(diagnostics) do - local start = diagnostic.range.start - -- TODO: Are diagnostics only valid for a single line? I don't understand - -- why this would be okay otherwise - local line_diagnostics = diagnostics_by_line[start.line] - if not line_diagnostics then - line_diagnostics = {} - diagnostics_by_line[start.line] = line_diagnostics - end - table.insert(line_diagnostics, diagnostic) - end - return diagnostics_by_line - end - - --- Given a list of diagnostics, sets the corresponding virtual text for a - --- buffer. - --- - --@param bufnr buffer id - --@param diagnostics (table) list of `Diagnostic`s - function M.buf_diagnostics_virtual_text(bufnr, diagnostics) - if not diagnostics then - return - end - local buffer_line_diagnostics = M.diagnostics_group_by_line(diagnostics) - for line, line_diags in pairs(buffer_line_diagnostics) do - local virt_texts = {} - for i = 1, #line_diags - 1 do - table.insert(virt_texts, {"■", severity_highlights[line_diags[i].severity]}) - end - local last = line_diags[#line_diags] - -- TODO(ashkan) use first line instead of subbing 2 spaces? - table.insert(virt_texts, {"■ "..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]}) - api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {}) - end - end - - --- Returns the number of diagnostics of given kind for current buffer. - --- - --- Useful for showing diagnostic counts in statusline. eg: - --- - --- <pre> - --- function! LspStatus() abort - --- let sl = '' - --- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))') - --- let sl.='%#MyStatuslineLSP#E:' - --- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Error]])")}' - --- let sl.='%#MyStatuslineLSP# W:' - --- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Warning]])")}' - --- else - --- let sl.='%#MyStatuslineLSPErrors#off' - --- endif - --- return sl - --- endfunction - --- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus() - --- </pre> - --- - --@param kind Diagnostic severity kind: See |vim.lsp.protocol.DiagnosticSeverity| - --@returns Count of diagnostics - function M.buf_diagnostics_count(kind) - local bufnr = vim.api.nvim_get_current_buf() - local diagnostics = M.diagnostics_by_buf[bufnr] - if not diagnostics then return end - local count = 0 - for _, diagnostic in pairs(diagnostics) do - if protocol.DiagnosticSeverity[kind] == diagnostic.severity then - count = count + 1 - end - end - return count - end - - local diagnostic_severity_map = { - [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign"; - [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign"; - [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign"; - [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign"; - } - - --- Places signs for each diagnostic in the sign column. - --- - --- Sign characters can be customized with the following commands: - --- - --- <pre> - --- sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl= - --- sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl= - --- sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl= - --- sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl= - --- </pre> - function M.buf_diagnostics_signs(bufnr, diagnostics) - for _, diagnostic in ipairs(diagnostics) do - vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)}) - end - end end local position_sort = sort_by_key(function(v) @@ -1561,6 +1358,9 @@ function M.character_offset(buf, row, col) return str_utfindex(line, col) end +M._get_line_byte_from_position = get_line_byte_from_position +M._warn_once = warn_once + M.buf_versions = {} return M diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 995c52e8ed..998e04f568 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -96,7 +96,7 @@ end --- --- Examples: --- <pre> ---- split(":aa::b:", ":") --> {'','aa','','bb',''} +--- split(":aa::b:", ":") --> {'','aa','','b',''} --- split("axaby", "ab?") --> {'','x','y'} --- split(x*yz*o, "*", true) --> {'x','yz','o'} --- </pre> @@ -496,7 +496,7 @@ do } local function _is_type(val, t) - return t == 'callable' and vim.is_callable(val) or type(val) == t + return type(val) == t or (t == 'callable' and vim.is_callable(val)) end local function is_valid(opt) diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index c42b568220..b1a7f92854 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -143,12 +143,13 @@ CONFIG = { 'section_start_token': '*lsp-core*', 'section_order': [ 'lsp.lua', - 'protocol.lua', 'buf.lua', - 'callbacks.lua', + 'diagnostic.lua', + 'handlers.lua', + 'util.lua', 'log.lua', 'rpc.lua', - 'util.lua' + 'protocol.lua', ], 'files': ' '.join([ os.path.join(base_dir, 'runtime/lua/vim/lsp'), @@ -447,7 +448,7 @@ def render_node(n, text, prefix='', indent='', width=62): indent=indent, width=width)) i = i + 1 elif n.nodeName == 'simplesect' and 'note' == n.getAttribute('kind'): - text += 'Note:\n ' + text += '\nNote:\n ' for c in n.childNodes: text += render_node(c, text, indent=' ', width=width) text += '\n' @@ -461,6 +462,8 @@ def render_node(n, text, prefix='', indent='', width=62): text += ind(' ') for c in n.childNodes: text += render_node(c, text, indent=' ', width=width) + elif n.nodeName == 'computeroutput': + return get_text(n) else: raise RuntimeError('unhandled node type: {}\n{}'.format( n.nodeName, n.toprettyxml(indent=' ', newl='\n'))) @@ -526,6 +529,7 @@ def para_as_map(parent, indent='', width=62): and is_inline(self_or_child(prev)) and is_inline(self_or_child(child)) and '' != get_text(self_or_child(child)).strip() + and text and ' ' != text[-1]): text += ' ' @@ -705,7 +709,7 @@ def extract_from_xml(filename, target, width): if len(prefix) + len(suffix) > lhs: signature = vimtag.rjust(width) + '\n' - signature += doc_wrap(suffix, width=width-8, prefix=prefix, + signature += doc_wrap(suffix, width=width, prefix=prefix, func=True) else: signature = prefix + suffix diff --git a/scripts/lua2dox.lua b/scripts/lua2dox.lua index d4e68f9e45..1dc4c0a5a0 100644 --- a/scripts/lua2dox.lua +++ b/scripts/lua2dox.lua @@ -73,7 +73,7 @@ function class(BaseClass, ClassInitialiser) local newInstance = {} setmetatable(newInstance,newClass) --if init then - -- init(newInstance,...) + -- init(newInstance,...) if class_tbl.init then class_tbl.init(newInstance,...) else @@ -214,7 +214,7 @@ TStream_Read = class() --! \brief get contents of file --! --! \param Filename name of file to read (or nil == stdin) -function TStream_Read.getContents(this,Filename) +function TStream_Read.getContents(this,Filename) -- get lines from file local filecontents if Filename then @@ -365,7 +365,7 @@ end --! \brief check comment for fn local function checkComment4fn(Fn_magic,MagicLines) local fn_magic = Fn_magic - -- TCore_IO_writeln('// checkComment4fn "' .. MagicLines .. '"') + -- TCore_IO_writeln('// checkComment4fn "' .. MagicLines .. '"') local magicLines = string_split(MagicLines,'\n') @@ -375,7 +375,7 @@ local function checkComment4fn(Fn_magic,MagicLines) macro,tail = getMagicDirective(line) if macro == 'fn' then fn_magic = tail - -- TCore_IO_writeln('// found fn "' .. fn_magic .. '"') + -- TCore_IO_writeln('// found fn "' .. fn_magic .. '"') else --TCore_IO_writeln('// not found fn "' .. line .. '"') end @@ -401,15 +401,23 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename) outStream:writelnTail('// #######################') outStream:writelnTail() - local state = '' + local state, offset = '', 0 while not (err or inStream:eof()) do line = string_trim(inStream:getLine()) - -- TCore_Debug_show_var('inStream',inStream) - -- TCore_Debug_show_var('line',line ) - if string.sub(line,1,2)=='--' then -- it's a comment - if string.sub(line,3,3)=='@' then -- it's a magic comment + -- TCore_Debug_show_var('inStream',inStream) + -- TCore_Debug_show_var('line',line ) + if string.sub(line,1,2) == '--' then -- it's a comment + -- Allow people to write style similar to EmmyLua (since they are basically the same) + -- instead of silently skipping things that start with --- + if string.sub(line, 3, 3) == '@' then -- it's a magic comment + offset = 0 + elseif string.sub(line, 1, 4) == '---@' then -- it's a magic comment + offset = 1 + end + + if string.sub(line, 3, 3) == '@' or string.sub(line, 1, 4) == '---@' then -- it's a magic comment state = 'in_magic_comment' - local magic = string.sub(line,4) + local magic = string.sub(line, 4 + offset) outStream:writeln('/// @' .. magic) fn_magic = checkComment4fn(fn_magic,magic) elseif string.sub(line,3,3)=='-' then -- it's a nonmagic doc comment @@ -450,7 +458,7 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename) outStream:writeln('// zz:"' .. line .. '"') fn_magic = nil end - elseif string.find(line,'^function') or string.find(line,'^local%s+function') then + elseif string.find(line, '^function') or string.find(line, '^local%s+function') then state = 'in_function' -- it's a function local pos_fn = string.find(line,'function') -- function @@ -490,6 +498,13 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename) this:warning(inStream:getLineNo(),'something weird here') end fn_magic = nil -- mustn't indavertently use it again + + -- TODO: If we can make this learn how to generate these, that would be helpful. + -- elseif string.find(line, "^M%['.*'%] = function") then + -- state = 'in_function' -- it's a function + -- outStream:writeln("function textDocument/publishDiagnostics(...){}") + + -- fn_magic = nil -- mustn't indavertently use it again else state = '' -- unknown if #line>0 then -- we don't know what this line means, so just comment it out diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 0580fcacae..2c7ab46ffe 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -272,6 +272,9 @@ local function __index(t, key) elseif key == 'highlight' then t.highlight = require('vim.highlight') return t.highlight + elseif key == 'F' then + t.F = require('vim.F') + return t.F end end diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index ec6accd473..5ce126a593 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -4296,8 +4296,9 @@ static void syn_cmd_include(exarg_T *eap, int syncing) current_syn_inc_tag = ++running_syn_inc_tag; prev_toplvl_grp = curwin->w_s->b_syn_topgrp; curwin->w_s->b_syn_topgrp = sgl_id; - if (source ? do_source(eap->arg, false, DOSO_NONE) == FAIL - : source_in_path(p_rtp, eap->arg, DIP_ALL) == FAIL) { + if (source + ? do_source(eap->arg, false, DOSO_NONE) == FAIL + : source_in_path(p_rtp, eap->arg, DIP_ALL) == FAIL) { EMSG2(_(e_notopen), eap->arg); } curwin->w_s->b_syn_topgrp = prev_toplvl_grp; diff --git a/test/functional/plugin/lsp/diagnostic_spec.lua b/test/functional/plugin/lsp/diagnostic_spec.lua new file mode 100644 index 0000000000..0fb55da4bd --- /dev/null +++ b/test/functional/plugin/lsp/diagnostic_spec.lua @@ -0,0 +1,767 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local exec_lua = helpers.exec_lua +local eq = helpers.eq +local nvim = helpers.nvim + +describe('vim.lsp.diagnostic', function() + local fake_uri + + before_each(function() + clear() + + exec_lua [[ + require('vim.lsp') + + make_range = function(x1, y1, x2, y2) + return { start = { line = x1, character = y1 }, ['end'] = { line = x2, character = y2 } } + end + + make_error = function(msg, x1, y1, x2, y2) + return { + range = make_range(x1, y1, x2, y2), + message = msg, + severity = 1, + } + end + + make_warning = function(msg, x1, y1, x2, y2) + return { + range = make_range(x1, y1, x2, y2), + message = msg, + severity = 2, + } + end + + make_information = function(msg, x1, y1, x2, y2) + return { + range = make_range(x1, y1, x2, y2), + message = msg, + severity = 3, + } + end + + count_of_extmarks_for_client = function(bufnr, client_id) + return #vim.api.nvim_buf_get_extmarks( + bufnr, vim.lsp.diagnostic._get_diagnostic_namespace(client_id), 0, -1, {} + ) + end + ]] + + fake_uri = "file://fake/uri" + + exec_lua([[ + fake_uri = ... + diagnostic_bufnr = vim.uri_to_bufnr(fake_uri) + local lines = {"1st line of text", "2nd line of text", "wow", "cool", "more", "lines"} + vim.fn.bufload(diagnostic_bufnr) + vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, 1, false, lines) + return diagnostic_bufnr + ]], fake_uri) + end) + + after_each(function() + clear() + end) + + describe('vim.lsp.diagnostic', function() + describe('handle_publish_diagnostics', function() + it('should be able to save and count a single client error', function() + eq(1, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #1', 1, 1, 1, 1), + }, 0, 1 + ) + return vim.lsp.diagnostic.get_count(0, "Error", 1) + ]]) + end) + + it('should be able to save and count from two clients', function() + eq(2, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #1', 1, 1, 1, 1), + make_error('Diagnostic #2', 2, 1, 2, 1), + }, 0, 1 + ) + return vim.lsp.diagnostic.get_count(0, "Error", 1) + ]]) + end) + + it('should be able to save and count from multiple clients', function() + eq({1, 1, 2}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic From Server 1', 1, 1, 1, 1), + }, 0, 1 + ) + vim.lsp.diagnostic.save( + { + make_error('Diagnostic From Server 2', 1, 1, 1, 1), + }, 0, 2 + ) + return { + -- Server 1 + vim.lsp.diagnostic.get_count(0, "Error", 1), + -- Server 2 + vim.lsp.diagnostic.get_count(0, "Error", 2), + -- All servers + vim.lsp.diagnostic.get_count(0, "Error", nil), + } + ]]) + end) + + it('should be able to save and count from multiple clients with respect to severity', function() + eq({3, 0, 3}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic From Server 1:1', 1, 1, 1, 1), + make_error('Diagnostic From Server 1:2', 2, 2, 2, 2), + make_error('Diagnostic From Server 1:3', 2, 3, 3, 2), + }, 0, 1 + ) + vim.lsp.diagnostic.save( + { + make_warning('Warning From Server 2', 3, 3, 3, 3), + }, 0, 2 + ) + return { + -- Server 1 + vim.lsp.diagnostic.get_count(0, "Error", 1), + -- Server 2 + vim.lsp.diagnostic.get_count(0, "Error", 2), + -- All servers + vim.lsp.diagnostic.get_count(0, "Error", nil), + } + ]]) + end) + + it('should handle one server clearing highlights while the other still has highlights', function() + -- 1 Error (1) + -- 1 Warning (2) + -- 1 Warning (2) + 1 Warning (1) + -- 2 highlights and 2 underlines (since error) + -- 1 highlight + 1 underline + local all_highlights = {1, 1, 2, 4, 2} + eq(all_highlights, exec_lua [[ + local server_1_diags = { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 2, 1, 2, 5), + } + local server_2_diags = { + make_warning("Warning 1", 2, 1, 2, 5), + } + + vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { uri = fake_uri, diagnostics = server_1_diags }, 1) + vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { uri = fake_uri, diagnostics = server_2_diags }, 2) + return { + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1), + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2), + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", nil), + count_of_extmarks_for_client(diagnostic_bufnr, 1), + count_of_extmarks_for_client(diagnostic_bufnr, 2), + } + ]]) + + -- Clear diagnostics from server 1, and make sure we have the right amount of stuff for client 2 + eq({1, 1, 2, 0, 2}, exec_lua [[ + vim.lsp.diagnostic.clear(diagnostic_bufnr, 1) + return { + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1), + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2), + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", nil), + count_of_extmarks_for_client(diagnostic_bufnr, 1), + count_of_extmarks_for_client(diagnostic_bufnr, 2), + } + ]]) + + -- Show diagnostics from server 1 again + eq(all_highlights, exec_lua([[ + vim.lsp.diagnostic.display(nil, diagnostic_bufnr, 1) + return { + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1), + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2), + vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", nil), + count_of_extmarks_for_client(diagnostic_bufnr, 1), + count_of_extmarks_for_client(diagnostic_bufnr, 2), + } + ]])) + end) + + describe('get_next_diagnostic_pos', function() + it('can find the next pos with only one client', function() + eq({1, 1}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #1', 1, 1, 1, 1), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + return vim.lsp.diagnostic.get_next_pos() + ]]) + end) + + it('can find next pos with two errors', function() + eq({4, 4}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #1', 1, 1, 1, 1), + make_error('Diagnostic #2', 4, 4, 4, 4), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.lsp.diagnostic.get_next_pos { client_id = 1 } + ]]) + end) + + it('can cycle when position is past error', function() + eq({1, 1}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #1', 1, 1, 1, 1), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.lsp.diagnostic.get_next_pos { client_id = 1 } + ]]) + end) + + it('will not cycle when wrap is off', function() + eq(false, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #1', 1, 1, 1, 1), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.lsp.diagnostic.get_next_pos { client_id = 1, wrap = false } + ]]) + end) + + it('can cycle even from the last line', function() + eq({4, 4}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #2', 4, 4, 4, 4), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {vim.api.nvim_buf_line_count(0), 1}) + return vim.lsp.diagnostic.get_prev_pos { client_id = 1 } + ]]) + end) + end) + + describe('get_prev_diagnostic_pos', function() + it('can find the prev pos with only one client', function() + eq({1, 1}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #1', 1, 1, 1, 1), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.lsp.diagnostic.get_prev_pos() + ]]) + end) + + it('can find prev pos with two errors', function() + eq({1, 1}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #1', 1, 1, 1, 1), + make_error('Diagnostic #2', 4, 4, 4, 4), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.lsp.diagnostic.get_prev_pos { client_id = 1 } + ]]) + end) + + it('can cycle when position is past error', function() + eq({4, 4}, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #2', 4, 4, 4, 4), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.lsp.diagnostic.get_prev_pos { client_id = 1 } + ]]) + end) + + it('respects wrap parameter', function() + eq(false, exec_lua [[ + vim.lsp.diagnostic.save( + { + make_error('Diagnostic #2', 4, 4, 4, 4), + }, diagnostic_bufnr, 1 + ) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.lsp.diagnostic.get_prev_pos { client_id = 1, wrap = false} + ]]) + end) + end) + end) + end) + + describe("vim.lsp.diagnostic.get_line_diagnostics", function() + it('should return an empty table when no diagnostics are present', function() + eq({}, exec_lua [[return vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1)]]) + end) + + it('should return all diagnostics when no severity is supplied', function() + eq(2, exec_lua [[ + vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 1, 1, 2, 5), + make_error("Error On Other Line", 2, 1, 1, 5), + } + }, 1) + + return #vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1) + ]]) + end) + + it('should return only requested diagnostics when severity_limit is supplied', function() + eq(2, exec_lua [[ + vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 1, 1, 2, 5), + make_information("Ignored information", 1, 1, 2, 5), + make_error("Error On Other Line", 2, 1, 1, 5), + } + }, 1) + + return #vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1, { severity_limit = "Warning" }) + ]]) + end) + end) + + describe("vim.lsp.diagnostic.on_publish_diagnostics", function() + it('can use functions for config values', function() + exec_lua [[ + vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + virtual_text = function() return true end, + })(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + } + }, 1 + ) + ]] + + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(2, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + + -- Now, don't enable virtual text. + -- We should have one less extmark displayed. + exec_lua [[ + vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + virtual_text = function() return false end, + })(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + } + }, 1 + ) + ]] + + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(1, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + end) + + it('can perform updates after insert_leave', function() + exec_lua [[vim.api.nvim_set_current_buf(diagnostic_bufnr)]] + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + -- Save the diagnostics + exec_lua [[ + vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + update_in_insert = false, + })(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + } + }, 1 + ) + ]] + + -- No diagnostics displayed yet. + eq({mode='i', blocking=false}, nvim("get_mode")) + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(0, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + + nvim("input", "<esc>") + eq({mode='n', blocking=false}, nvim("get_mode")) + + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(2, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + end) + + it('does not perform updates when not needed', function() + exec_lua [[vim.api.nvim_set_current_buf(diagnostic_bufnr)]] + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + -- Save the diagnostics + exec_lua [[ + PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + update_in_insert = false, + virtual_text = true, + }) + + -- Count how many times we call display. + SetVirtualTextOriginal = vim.lsp.diagnostic.set_virtual_text + + DisplayCount = 0 + vim.lsp.diagnostic.set_virtual_text = function(...) + DisplayCount = DisplayCount + 1 + return SetVirtualTextOriginal(...) + end + + PublishDiagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + } + }, 1 + ) + ]] + + -- No diagnostics displayed yet. + eq({mode='i', blocking=false}, nvim("get_mode")) + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(0, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + eq(0, exec_lua [[return DisplayCount]]) + + nvim("input", "<esc>") + eq({mode='n', blocking=false}, nvim("get_mode")) + + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(2, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + eq(1, exec_lua [[return DisplayCount]]) + + -- Go in and out of insert mode one more time. + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + nvim("input", "<esc>") + eq({mode='n', blocking=false}, nvim("get_mode")) + + -- Should not have set the virtual text again. + eq(1, exec_lua [[return DisplayCount]]) + end) + + it('never sets virtual text, in combination with insert leave', function() + exec_lua [[vim.api.nvim_set_current_buf(diagnostic_bufnr)]] + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + -- Save the diagnostics + exec_lua [[ + PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + update_in_insert = false, + virtual_text = false, + }) + + -- Count how many times we call display. + SetVirtualTextOriginal = vim.lsp.diagnostic.set_virtual_text + + DisplayCount = 0 + vim.lsp.diagnostic.set_virtual_text = function(...) + DisplayCount = DisplayCount + 1 + return SetVirtualTextOriginal(...) + end + + PublishDiagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + } + }, 1 + ) + ]] + + -- No diagnostics displayed yet. + eq({mode='i', blocking=false}, nvim("get_mode")) + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(0, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + eq(0, exec_lua [[return DisplayCount]]) + + nvim("input", "<esc>") + eq({mode='n', blocking=false}, nvim("get_mode")) + + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(1, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + eq(0, exec_lua [[return DisplayCount]]) + + -- Go in and out of insert mode one more time. + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + nvim("input", "<esc>") + eq({mode='n', blocking=false}, nvim("get_mode")) + + -- Should not have set the virtual text still. + eq(0, exec_lua [[return DisplayCount]]) + end) + + it('can perform updates while in insert mode, if desired', function() + exec_lua [[vim.api.nvim_set_current_buf(diagnostic_bufnr)]] + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + -- Save the diagnostics + exec_lua [[ + vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + update_in_insert = true, + })(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + } + }, 1 + ) + ]] + + -- Diagnostics are displayed, because the user wanted them that way! + eq({mode='i', blocking=false}, nvim("get_mode")) + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(2, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + + nvim("input", "<esc>") + eq({mode='n', blocking=false}, nvim("get_mode")) + + eq(1, exec_lua [[return vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1)]]) + eq(2, exec_lua [[return count_of_extmarks_for_client(diagnostic_bufnr, 1)]]) + end) + + it('allows configuring the virtual text via vim.lsp.with', function() + local expected_spacing = 10 + local extmarks = exec_lua([[ + PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + virtual_text = { + spacing = ..., + }, + }) + + PublishDiagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + } + }, 1 + ) + + return vim.api.nvim_buf_get_extmarks( + diagnostic_bufnr, + vim.lsp.diagnostic._get_diagnostic_namespace(1), + 0, + -1, + { details = true } + ) + ]], expected_spacing) + + local virt_text = extmarks[1][4].virt_text + local spacing = virt_text[1][1] + + eq(expected_spacing, #spacing) + end) + + + it('allows configuring the virtual text via vim.lsp.with using a function', function() + local expected_spacing = 10 + local extmarks = exec_lua([[ + spacing = ... + + PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + virtual_text = function() + return { + spacing = spacing, + } + end, + }) + + PublishDiagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + } + }, 1 + ) + + return vim.api.nvim_buf_get_extmarks( + diagnostic_bufnr, + vim.lsp.diagnostic._get_diagnostic_namespace(1), + 0, + -1, + { details = true } + ) + ]], expected_spacing) + + local virt_text = extmarks[1][4].virt_text + local spacing = virt_text[1][1] + + eq(expected_spacing, #spacing) + end) + end) + + describe('lsp.util.show_line_diagnostics', function() + it('creates floating window and returns popup bufnr and winnr if current line contains diagnostics', function() + -- Two lines: + -- Diagnostic: + -- 1. <msg> + eq(2, exec_lua [[ + local buffer = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, { + "testing"; + "123"; + }) + local diagnostics = { + { + range = { + start = { line = 0; character = 1; }; + ["end"] = { line = 0; character = 3; }; + }; + severity = vim.lsp.protocol.DiagnosticSeverity.Error; + message = "Syntax error"; + }, + } + vim.api.nvim_win_set_buf(0, buffer) + vim.lsp.diagnostic.save(diagnostics, buffer, 1) + local popup_bufnr, winnr = vim.lsp.diagnostic.show_line_diagnostics() + return #vim.api.nvim_buf_get_lines(popup_bufnr, 0, -1, false) + ]]) + end) + + it('creates floating window and returns popup bufnr and winnr without header, if requested', function() + -- One line (since no header): + -- 1. <msg> + eq(1, exec_lua [[ + local buffer = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, { + "testing"; + "123"; + }) + local diagnostics = { + { + range = { + start = { line = 0; character = 1; }; + ["end"] = { line = 0; character = 3; }; + }; + severity = vim.lsp.protocol.DiagnosticSeverity.Error; + message = "Syntax error"; + }, + } + vim.api.nvim_win_set_buf(0, buffer) + vim.lsp.diagnostic.save(diagnostics, buffer, 1) + local popup_bufnr, winnr = vim.lsp.diagnostic.show_line_diagnostics { show_header = false } + return #vim.api.nvim_buf_get_lines(popup_bufnr, 0, -1, false) + ]]) + end) + end) + + describe('set_signs', function() + -- TODO(tjdevries): Find out why signs are not displayed when set from Lua...?? + pending('sets signs by default', function() + exec_lua [[ + PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + update_in_insert = true, + signs = true, + }) + + local diagnostics = { + make_error('Delayed Diagnostic', 1, 1, 1, 2), + make_error('Delayed Diagnostic', 3, 3, 3, 3), + } + + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { + uri = fake_uri, + diagnostics = diagnostics + }, 1 + ) + + vim.lsp.diagnostic.set_signs(diagnostics, diagnostic_bufnr, 1) + -- return vim.fn.sign_getplaced() + ]] + + nvim("input", "o") + nvim("input", "<esc>") + + -- TODO(tjdevries): Find a way to get the signs to display in the test... + eq(nil, exec_lua [[ + return im.fn.sign_getplaced()[1].signs + ]]) + end) + end) + + describe('set_loclist()', function() + it('sets diagnostics in lnum order', function() + local loc_list = exec_lua [[ + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + + vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Farther Diagnostic', 4, 4, 4, 4), + make_error('Lower Diagnostic', 1, 1, 1, 1), + } + }, 1 + ) + + vim.lsp.diagnostic.set_loclist() + + return vim.fn.getloclist(0) + ]] + + assert(loc_list[1].lnum < loc_list[2].lnum) + end) + + it('sets diagnostics in lnum order, regardless of client', function() + local loc_list = exec_lua [[ + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + + vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_error('Lower Diagnostic', 1, 1, 1, 1), + } + }, 1 + ) + + vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { + uri = fake_uri, + diagnostics = { + make_warning('Farther Diagnostic', 4, 4, 4, 4), + } + }, 2 + ) + + vim.lsp.diagnostic.set_loclist() + + return vim.fn.getloclist(0) + ]] + + assert(loc_list[1].lnum < loc_list[2].lnum) + end) + end) +end) diff --git a/test/functional/plugin/lsp/handler_spec.lua b/test/functional/plugin/lsp/handler_spec.lua new file mode 100644 index 0000000000..3086c23fe8 --- /dev/null +++ b/test/functional/plugin/lsp/handler_spec.lua @@ -0,0 +1,29 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local exec_lua = helpers.exec_lua +local pcall_err = helpers.pcall_err +local matches = helpers.matches + +describe('lsp-handlers', function() + describe('vim.lsp._with_extend', function() + it('should return a table with the default keys', function() + eq({hello = 'world' }, exec_lua [[ + return vim.lsp._with_extend('test', { hello = 'world' }) + ]]) + end) + + it('should override with config keys', function() + eq({hello = 'universe', other = true}, exec_lua [[ + return vim.lsp._with_extend('test', { other = true, hello = 'world' }, { hello = 'universe' }) + ]]) + end) + + it('should not allow invalid keys', function() + matches( + '.*Invalid option for `test`.*', + pcall_err(exec_lua, "return vim.lsp._with_extend('test', { hello = 'world' }, { invalid = true })") + ) + end) + end) +end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 00093f71d4..5b048f57e9 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -323,7 +323,7 @@ describe('LSP', function() test_name = "capabilities_for_client_supports_method"; on_setup = function() exec_lua([=[ - vim.lsp.callbacks['textDocument/hover'] = function(err, method) + vim.lsp.handlers['textDocument/hover'] = function(err, method) vim.lsp._last_lsp_callback = { err = err; method = method } end vim.lsp._unsupported_method = function(method) @@ -847,25 +847,28 @@ describe('LSP', function() end it('highlight groups', function() - eq({'LspDiagnosticsError', - 'LspDiagnosticsErrorFloating', - 'LspDiagnosticsErrorSign', - 'LspDiagnosticsHint', - 'LspDiagnosticsHintFloating', - 'LspDiagnosticsHintSign', - 'LspDiagnosticsInformation', - 'LspDiagnosticsInformationFloating', - 'LspDiagnosticsInformationSign', - 'LspDiagnosticsUnderline', - 'LspDiagnosticsUnderlineError', - 'LspDiagnosticsUnderlineHint', - 'LspDiagnosticsUnderlineInformation', - 'LspDiagnosticsUnderlineWarning', - 'LspDiagnosticsWarning', - 'LspDiagnosticsWarningFloating', - 'LspDiagnosticsWarningSign', - }, - exec_lua([[require'vim.lsp'; return vim.fn.getcompletion('Lsp', 'highlight')]])) + eq({ + 'LspDiagnosticsDefaultError', + 'LspDiagnosticsDefaultHint', + 'LspDiagnosticsDefaultInformation', + 'LspDiagnosticsDefaultWarning', + 'LspDiagnosticsFloatingError', + 'LspDiagnosticsFloatingHint', + 'LspDiagnosticsFloatingInformation', + 'LspDiagnosticsFloatingWarning', + 'LspDiagnosticsSignError', + 'LspDiagnosticsSignHint', + 'LspDiagnosticsSignInformation', + 'LspDiagnosticsSignWarning', + 'LspDiagnosticsUnderlineError', + 'LspDiagnosticsUnderlineHint', + 'LspDiagnosticsUnderlineInformation', + 'LspDiagnosticsUnderlineWarning', + 'LspDiagnosticsVirtualTextError', + 'LspDiagnosticsVirtualTextHint', + 'LspDiagnosticsVirtualTextInformation', + 'LspDiagnosticsVirtualTextWarning', + }, exec_lua([[require'vim.lsp'; return vim.fn.getcompletion('Lsp', 'highlight')]])) end) describe('apply_text_edits', function() @@ -1037,7 +1040,7 @@ describe('LSP', function() label = nil; edit = {}; } - return vim.lsp.callbacks['workspace/applyEdit'](nil, nil, apply_edit) + return vim.lsp.handlers['workspace/applyEdit'](nil, nil, apply_edit) ]]) end) end) @@ -1084,47 +1087,7 @@ describe('LSP', function() eq({}, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], {}, prefix)) end) end) - describe('buf_diagnostics_save_positions', function() - it('stores the diagnostics in diagnostics_by_buf', function () - local diagnostics = { - { range = {}; message = "diag1" }, - { range = {}; message = "diag2" }, - } - exec_lua([[ - vim.lsp.util.buf_diagnostics_save_positions(...)]], 0, diagnostics) - eq(1, exec_lua [[ return #vim.lsp.util.diagnostics_by_buf ]]) - eq(diagnostics, exec_lua [[ - for _, diagnostics in pairs(vim.lsp.util.diagnostics_by_buf) do - return diagnostics - end - ]]) - end) - end) - describe('lsp.util.show_line_diagnostics', function() - it('creates floating window and returns popup bufnr and winnr if current line contains diagnostics', function() - eq(3, exec_lua [[ - local buffer = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(buffer, 0, -1, false, { - "testing"; - "123"; - }) - local diagnostics = { - { - range = { - start = { line = 0; character = 1; }; - ["end"] = { line = 0; character = 3; }; - }; - severity = vim.lsp.protocol.DiagnosticSeverity.Error; - message = "Syntax error"; - }, - } - vim.api.nvim_win_set_buf(0, buffer) - vim.lsp.util.buf_diagnostics_save_positions(vim.fn.bufnr(buffer), diagnostics) - local popup_bufnr, winnr = vim.lsp.util.show_line_diagnostics() - return popup_bufnr - ]]) - end) - end) + describe('lsp.util.locations_to_items', function() it('Convert Location[] to items', function() local expected = { @@ -1556,7 +1519,7 @@ describe('LSP', function() describe('vim.lsp.buf.outgoing_calls', function() it('does nothing for an empty response', function() local qflist_count = exec_lua([=[ - require'vim.lsp.callbacks'['callHierarchy/outgoingCalls']() + require'vim.lsp.handlers'['callHierarchy/outgoingCalls']() return #vim.fn.getqflist() ]=]) eq(0, qflist_count) @@ -1602,7 +1565,7 @@ describe('LSP', function() uri = "file:///src/main.rs" } } } - local callback = require'vim.lsp.callbacks'['callHierarchy/outgoingCalls'] + local callback = require'vim.lsp.handlers'['callHierarchy/outgoingCalls'] callback(nil, nil, rust_analyzer_response) return vim.fn.getqflist() ]=]) @@ -1627,7 +1590,7 @@ describe('LSP', function() describe('vim.lsp.buf.incoming_calls', function() it('does nothing for an empty response', function() local qflist_count = exec_lua([=[ - require'vim.lsp.callbacks'['callHierarchy/incomingCalls']() + require'vim.lsp.handlers'['callHierarchy/incomingCalls']() return #vim.fn.getqflist() ]=]) eq(0, qflist_count) @@ -1674,7 +1637,7 @@ describe('LSP', function() } } } } - local callback = require'vim.lsp.callbacks'['callHierarchy/incomingCalls'] + local callback = require'vim.lsp.handlers'['callHierarchy/incomingCalls'] callback(nil, nil, rust_analyzer_response) return vim.fn.getqflist() ]=]) |