Skip to content
forked from neovim/neovim

Commit

Permalink
feat(lua): vim.on_fun()
Browse files Browse the repository at this point in the history
Problem:
No easy way to hook into LSP request/response handlers. (No mention of
"before" or "after" in `:help lsp`...)

Solution:
Introduce vim.on_fun(), a generic way to "hook into" any function
before/after it is called.

Example:

    vim.on_fun(vim.lsp.handlers, 'textDocument/definition', function()
      vim.print('before, yay')
    end)

Fixes neovim#20568
Fixes neovim#22075
Fixes neovim#22323
Related: neovim#13977
  • Loading branch information
justinmk committed Mar 11, 2023
1 parent 8a3220b commit 30faab6
Show file tree
Hide file tree
Showing 16 changed files with 634 additions and 143 deletions.
1 change: 1 addition & 0 deletions runtime/doc/deprecated.txt
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ LSP FUNCTIONS
{async = true} instead.
- *vim.lsp.buf.range_formatting()* Use |vim.lsp.formatexpr()|
or |vim.lsp.buf.format()| instead.
- *vim.lsp.with()* Use |vim.on_fun()| instead.

TREESITTER FUNCTIONS
- *vim.treesitter.language.require_language()* Use |vim.treesitter.add()|
Expand Down
2 changes: 2 additions & 0 deletions runtime/doc/develop.txt
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ Standard Features ~

External UIs are expected to implement these common features:

- Call |nvim_paste()| when the user pastes text (CTRL-v/CMD-v/…). Don't
directly call "p", nvim_input(), etc.
- Call |nvim_set_client_info()| after connecting, so users and plugins can
detect the UI by handling the |ChanInfo| event. This avoids the need for
special variables and UI-specific config files (gvimrc, macvimrc, …).
Expand Down
172 changes: 65 additions & 107 deletions runtime/doc/lsp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,20 @@ QUICKSTART *lsp-quickstart*
Nvim provides an LSP client, but the servers are provided by third parties.
Follow these steps to get LSP features:

1. Install language servers using your package manager or by
following the upstream installation instruction.

A list of language servers is available at:

1. Install language servers using your package manager or by following the
upstream installation instructions.
https://microsoft.github.io/language-server-protocol/implementors/servers/

2. Configure the LSP client per language server.
A minimal example:
>lua
vim.lsp.start({
name = 'my-server-name',
cmd = {'name-of-language-server-executable'},
root_dir = vim.fs.dirname(vim.fs.find({'setup.py', 'pyproject.toml'}, { upward = true })[1]),
})
<
See |vim.lsp.start()| for details.
2. Configure the LSP client per language server. See |vim.lsp.start()| for
details. Minimal example: >lua

3. Configure keymaps and autocmds to utilize LSP features.
See |lsp-config|.
vim.lsp.start({
name = 'my-server-name',
cmd = {'name-of-language-server-executable'},
root_dir = vim.fs.dirname(vim.fs.find({'setup.py', 'pyproject.toml'}, { upward = true })[1]),
})
<
3. Configure keymaps and autocmds to utilize LSP features. See |lsp-config|.

*lsp-config*

Expand Down Expand Up @@ -241,7 +235,7 @@ For |lsp-request|, each |lsp-handler| has this signature: >
particular handler.

To configure a particular |lsp-handler|, see:
|lsp-handler-configuration|
|lsp-handler-config|


Returns: ~
Expand Down Expand Up @@ -282,105 +276,83 @@ For |lsp-notification|, each |lsp-handler| has this signature: >
|vim.lsp.diagnostic.on_publish_diagnostics()|

To configure a particular |lsp-handler|, see:
|lsp-handler-configuration|
|lsp-handler-config|

Returns: ~
The |lsp-handler|'s return value will be ignored.

*lsp-handler-configuration*
*lsp-handler-config*

To configure the behavior of a builtin |lsp-handler|, the convenient method
|vim.lsp.with()| is provided for users.
To override or augment a builtin |lsp-handler|, use |vim.on_fun()|.

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: >lua
Example: this code overrides |vim.lsp.diagnostic.on_publish_diagnostics()|, to
disable signs for diagnostics: >lua

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: >lua

vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
vim.lsp.diagnostic.on_publish_diagnostics, {
-- Enable signs
signs = true,
}
)
vim.on_fun(vim.lsp.handlers, 'textDocument/publishDiagnostics',
function(err, result, ctx, config)
config.signs = false, -- Disable signs
end)
<
To configure a handler on a per-server basis, you can use the {handlers} key
for |vim.lsp.start_client()| >lua
To enable signs, use vim.on_fun() to override the default handler: >lua

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,
}
),
},
}
vim.on_fun(vim.lsp.handlers, 'textDocument/publishDiagnostics',
function(err, result, ctx, config)
config.signs = true, -- Enable signs.
end)
<
To configure handlers per-server you can use the {handlers} key of
|vim.lsp.start()|: >lua

vim.lsp.start{
handlers = {
["textDocument/publishDiagnostics"] = vim.wrap(vim.lsp.diagnostic.on_publish_diagnostics,
function(err, result, ctx, config)
config.virtual_text = false, -- Disable virtual text.
end)
},
..., -- Other configuration omitted.
}
<
or if using "nvim-lspconfig", you can use the {handlers} key of `setup()`:
>lua
or if using "nvim-lspconfig", use the {handlers} key of `setup()`: >lua

require('lspconfig').rust_analyzer.setup {
handlers = {
["textDocument/publishDiagnostics"] = vim.lsp.with(
vim.lsp.diagnostic.on_publish_diagnostics, {
-- Disable virtual_text
virtual_text = false
}
),
}
require('lspconfig').rust_analyzer.setup {
handlers = {
-- (same as above)
}
}
<
Some handlers do not have an explicitly named handler function (such as
||vim.lsp.diagnostic.on_publish_diagnostics()|). To override these, first
create a reference to the existing handler: >lua

local on_references = vim.lsp.handlers["textDocument/references"]
vim.lsp.handlers["textDocument/references"] = vim.lsp.with(
on_references, {
-- Use location list instead of quickfix list
loclist = true,
}
)
<
*lsp-handler-resolution*
Handlers can be set by:
Most handlers do not have a dedicated handler function (such as
||vim.lsp.diagnostic.on_publish_diagnostics()|).

- 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|.
*vim.lsp.handlers* *lsp-handler-resolution*
The |lsp-handler| is resolved based on the current |lsp-method| in the
following order:

To override the handler for the `"textDocument/definition"` method: >lua
1. Handler (if any) passed to |vim.lsp.buf_request()|.
2. Handler (if any) defined in |vim.lsp.start()|.
3. Handler (if any) defined on |vim.lsp.handlers|.

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.
Handlers can be set by:
- Global table `vim.lsp.handlers` defining the default mapping of |lsp-method|
names to |lsp-handlers|. Use |vim.on_fun()| to hook into the default
handler for the "textDocument/definition" method: >lua

vim.on_fun(vim.lsp.handlers, 'textDocument/definition', function(...)
-- custom logic
end)

For example: >lua
- {handlers} parameter of |vim.lsp.start()|. This will set the |lsp-handler|
as the default handler for this server. Example: >lua

vim.lsp.start_client {
vim.lsp.start {
..., -- Other configuration omitted.
handlers = {
["textDocument/definition"] = my_custom_server_definition
},
}

- The {handler} parameter for |vim.lsp.buf_request()|.
This will set the |lsp-handler| ONLY for the current request.

For example: >lua
- {handler} parameter of |vim.lsp.buf_request()|. This will set the
|lsp-handler| ONLY for the current request. Example: >lua

vim.lsp.buf_request(
0,
Expand All @@ -389,12 +361,6 @@ Handlers can be set by:
my_request_custom_definition
)
<
In summary, the |lsp-handler| will be chosen based on the current |lsp-method|
in the following order:

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.log_levels*
Log levels are defined in |vim.log.levels|
Expand Down Expand Up @@ -1074,14 +1040,6 @@ tagfunc({...}) *vim.lsp.tagfunc()*
Return: ~
table[] tags A list of matching tags

with({handler}, {override_config}) *vim.lsp.with()*
Function to manage overriding defaults for LSP handlers.

Parameters: ~
{handler} (function) See |lsp-handler|
• {override_config} (table) Table containing the keys to override
behavior of the {handler}


==============================================================================
Lua module: vim.lsp.buf *lsp-buf*
Expand Down
85 changes: 75 additions & 10 deletions runtime/doc/lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,71 @@ notify_once({msg}, {level}, {opts}) *vim.notify_once()*
Return: ~
(boolean) true if message was displayed, else false

on_fun({container}, {key}, {fn}) *vim.on_fun()*
Sets function `container[key]` to a new (wrapper) function that calls
`fn()` before optionally calling the original ("base") function.

The result of `fn()` decides how the base function is invoked. Given `fn()` : >
function()
return [r1, […, rn]]
end
<
• no result: invoke base function with the original args.
• r1=false: skip base function; wrapper returns `[…, rn]`.
• r1=true: invoke base function with args `[…, rn]`, or original args if
`[…, rn]` is empty.
• r1=function: like "r1=true", and invoke this "after" function after
invoking the base function. Result of "after function" decides the
return value(s) of the wrapped function.

Modification of container-like parameters by fn() affects the parameters
passed to the base function.

Example: increment a counter when vim.paste() is called. >
vim.on_fun(vim, 'paste', function()
counter = counter + 1
end)
<

Example: to modify the config during an LSP request (like the old `vim.lsp.with()` function): >
vim.on_fun(vim.lsp.handlers, 'textDocument/definition', function(err, result, ctx, config)
return true, err, result, ctx, vim.tbl_deep_extend('force', config or {}, override_config)
end)
<

Compared to this Lua idiom: >lua

vim.foo = (function(basefn)
return function(...)
-- do stuff...
basefn(...)
end
end)(vim.foo)
<

the difference is that vim.on_fun:
• ✓ XXX ??? idempontent (safe to call redundantly with identical args,
will be ignored)
• ✓ supports :unhook()
• ✓ supports inspection (visiblity)
• ✗ does logging
• ✗ supports auto-removal tied to a container scope (via weaktables)
• ✗ supports buf/win/tab-local definitions
• ✗ supports namespaces

Parameters: ~
{container} (table)
{key} (string)
{fn} (function) If the first return value is `false`, the
original function is skipped.

Return: ~
(table) obj with original function and :unwind()

on_key({fn}, {ns_id}) *vim.on_key()*
Adds Lua function {fn} with namespace id {ns_id} as a listener to every,
yes every, input key.
Expand Down Expand Up @@ -1484,17 +1549,17 @@ paste({lines}, {phase}) *vim.paste()*
Parameters: ~
{lines} string[] # |readfile()|-style list of lines to paste.
|channel-lines|
{phase} paste_phase -1: "non-streaming" paste: the call contains all
lines. If paste is "streamed", `phase` indicates the stream state:
{phase} (integer) indicates the stream state:
• 1: starts the paste (exactly once)
• 2: continues the paste (zero or more times)
• 3: ends the paste (exactly once)
• -1: "non-streamed" paste, the call contains all lines.

Return: ~
(boolean) # false if client should cancel the paste.

See also: ~
|paste| @alias paste_phase -1 | 1 | 2 | 3
|paste|

pretty_print({...}) *vim.pretty_print()*
Prints given arguments in human-readable format. Example: >lua
Expand Down Expand Up @@ -1863,22 +1928,22 @@ tbl_flatten({t}) *vim.tbl_flatten()*
From https://github.com/premake/premake-core/blob/master/src/base/table.lua

tbl_get({o}, {...}) *vim.tbl_get()*
Index into a table (first argument) via string keys passed as subsequent
arguments. Return `nil` if the key does not exist.
Index into table `o` via string keys or number indices `...`.

Examples: >lua

vim.tbl_get({ key = { nested_key = true }}, 'key', 'nested_key') == true
vim.tbl_get({ key = {}}, 'key', 'nested_key') == nil
vim.tbl_get({ k1 = { k2 = true }}, 'k1', 'k2') == true
vim.tbl_get({ k1 = {}}, 'k1', 'k2') == nil
vim.tbl_get({ k1 = {}}, 'k1', 'k2') == nil
<

Parameters: ~
{o} (table) Table to index
{...} (string) Optional strings (0 or more, variadic) via which to
index the table
{...} string|number sequence of string or number keys representing
the query

Return: ~
any Nested value indexed by key (if it exists), else nil
any any Nested value indexed by key, or nil if not found

tbl_isempty({t}) *vim.tbl_isempty()*
Checks if a table is empty.
Expand Down

0 comments on commit 30faab6

Please sign in to comment.