diff options
author | TJ DeVries <devries.timothyj@gmail.com> | 2020-11-12 22:21:34 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-12 22:21:34 -0500 |
commit | f75be5e9d510d5369c572cf98e78d9480df3b0bb (patch) | |
tree | e25baab19bcb47ca0d2edcf0baa18b71cfd03f9e | |
parent | 4ae31c46f75aef7d7a80dd2a8d269c168806a1bd (diff) | |
download | rneovim-f75be5e9d510d5369c572cf98e78d9480df3b0bb.tar.gz rneovim-f75be5e9d510d5369c572cf98e78d9480df3b0bb.tar.bz2 rneovim-f75be5e9d510d5369c572cf98e78d9480df3b0bb.zip |
lsp: vim.lsp.diagnostic (#12655)
Breaking Changes:
- Deprecated all `vim.lsp.util.{*diagnostics*}()` functions.
- Instead, all functions must be found in vim.lsp.diagnostic
- For now, they issue a warning ONCE per neovim session. In a
"little while" we will remove them completely.
- `vim.lsp.callbacks` has moved to `vim.lsp.handlers`.
- For a "little while" we will just redirect `vim.lsp.callbacks` to
`vim.lsp.handlers`. However, we will remove this at some point, so
it is recommended that you change all of your references to
`callbacks` into `handlers`.
- This also means that for functions like |vim.lsp.start_client()|
and similar, keyword style arguments have moved from "callbacks"
to "handlers". Once again, these are currently being forward, but
will cease to be forwarded in a "little while".
- Changed the highlight groups for LspDiagnostic highlight as they were
inconsistently named.
- For more information, see |lsp-highlight-diagnostics|
- Changed the sign group names as well, to be consistent with
|lsp-highlight-diagnostics|
General Enhancements:
- Rewrote much of the getting started help document for lsp. It also
provides a much nicer configuration strategy, so as to not recommend
globally overwriting builtin neovim mappings.
LSP Enhancements:
- Introduced the concept of |lsp-handlers| which will allow much better
customization for users without having to copy & paste entire files /
functions / etc.
Diagnostic Enhancements:
- "goto next diagnostic" |vim.lsp.diagnostic.goto_next()|
- "goto prev diagnostic" |vim.lsp.diagnostic.goto_prev()|
- For each of the gotos, auto open diagnostics is available as a
configuration option
- Configurable diagnostic handling:
- See |vim.lsp.diagnostic.on_publish_diagnostics()|
- Delay display until after insert mode
- Configure signs
- Configure virtual text
- Configure underline
- Set the location list with the buffers diagnostics.
- See |vim.lsp.diagnostic.set_loclist()|
- Better performance for getting counts and line diagnostics
- They are now cached on save, to enhance lookups.
- Particularly useful for checking in statusline, etc.
- Actual testing :)
- See ./test/functional/plugin/lsp/diagnostic_spec.lua
- Added `guisp` for underline highlighting
NOTE: "a little while" means enough time to feel like most plugins and
plugin authors have had a chance to refactor their code to use the
updated calls. Then we will remove them completely. There is no need to
keep them, because we don't have any released version of neovim that
exposes these APIs. I'm trying to be nice to people following HEAD :)
Co-authored: [Twitch Chat 2020](https://twitch.tv/teej_dv)
-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() ]=]) |