aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/api.txt53
-rw-r--r--runtime/doc/lsp-extension.txt129
-rw-r--r--runtime/doc/lsp.txt1350
-rw-r--r--runtime/doc/lua.txt1
-rw-r--r--runtime/lua/vim/F.lua24
-rw-r--r--runtime/lua/vim/highlight.lua16
-rw-r--r--runtime/lua/vim/lsp.lua216
-rw-r--r--runtime/lua/vim/lsp/buf.lua15
-rw-r--r--runtime/lua/vim/lsp/callbacks.lua345
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua1195
-rw-r--r--runtime/lua/vim/lsp/handlers.lua310
-rw-r--r--runtime/lua/vim/lsp/protocol.lua25
-rw-r--r--runtime/lua/vim/lsp/rpc.lua68
-rw-r--r--runtime/lua/vim/lsp/util.lua402
-rw-r--r--runtime/lua/vim/shared.lua4
-rwxr-xr-xscripts/gen_vimdoc.py14
-rw-r--r--scripts/lua2dox.lua37
-rw-r--r--src/nvim/lua/vim.lua3
-rw-r--r--src/nvim/syntax.c5
-rw-r--r--test/functional/plugin/lsp/diagnostic_spec.lua767
-rw-r--r--test/functional/plugin/lsp/handler_spec.lua29
-rw-r--r--test/functional/plugin/lsp_spec.lua95
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()
]=])