feat(lsp): add vim.lsp.config and vim.lsp.enable

Design goals/requirements:
- Default configuration of a server can be distributed across multiple sources.
  - And via RTP discovery.
- Default configuration can be specified for all servers.
- Configuration _can_ be project specific.

Solution:

- Two new API's:
  - `vim.lsp.config(name, cfg)`:
    - Used to define default configurations for servers of name.
    - Can be used like a table or called as a function.
    - Use `vim.lsp.confg('*', cfg)` to specify default config for all
      servers.
  - `vim.lsp.enable(name)`
    - Used to enable servers of name. Uses configuration defined
    via `vim.lsp.config()`.
This commit is contained in:
Lewis Russell 2024-11-01 16:31:51 +00:00 committed by Lewis Russell
parent ca760e645b
commit 3f1d09bc94
11 changed files with 600 additions and 75 deletions

View File

@ -28,31 +28,114 @@ Follow these steps to get LSP features:
upstream installation instructions. You can find language servers here: upstream installation instructions. You can find language servers here:
https://microsoft.github.io/language-server-protocol/implementors/servers/ https://microsoft.github.io/language-server-protocol/implementors/servers/
2. Use |vim.lsp.start()| to start the LSP server (or attach to an existing 2. Use |vim.lsp.config()| to define a configuration for an LSP client.
one) when a file is opened. Example: >lua Example: >lua
-- Create an event handler for the FileType autocommand vim.lsp.config['luals'] = {
vim.api.nvim_create_autocmd('FileType', { -- Command and arguments to start the server.
-- This handler will fire when the buffer's 'filetype' is "python" cmd = { 'lua-language-server' }
pattern = 'python',
callback = function(args)
vim.lsp.start({
name = 'my-server-name',
cmd = {'name-of-language-server-executable', '--option', 'arg1', 'arg2'},
-- Set the "root directory" to the parent directory of the file in the -- Filetypes to automatically attach to.
-- current buffer (`args.buf`) that contains either a "setup.py" or a filetypes = { 'lua' },
-- "pyproject.toml" file. Files that share a root directory will reuse
-- the connection to the same LSP server. -- Sets the "root directory" to the parent directory of the file in the
root_dir = vim.fs.root(args.buf, {'setup.py', 'pyproject.toml'}), -- current buffer that contains either a ".luarc.json" or a
}) -- ".luarc.jsonc" file. Files that share a root directory will reuse
end, -- the connection to the same LSP server.
}) root_markers = { '.luarc.json', '.luarc.jsonc' },
-- Specific settings to send to the server. The schema for this is
-- defined by the server. For example the schema for lua-language-server
-- can be found here https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json
settings = {
Lua = {
runtime = {
version = 'LuaJIT',
}
}
}
}
< <
3. Check that the buffer is attached to the server: >vim 3. Use |vim.lsp.enable()| to enable a configuration.
:checkhealth lsp Example: >lua
vim.lsp.enable('luals')
<
4. Check that the buffer is attached to the server: >vim
:checkhealth vim.lsp
<
5. (Optional) Configure keymaps and autocommands to use LSP features.
|lsp-attach|
4. (Optional) Configure keymaps and autocommands to use LSP features. |lsp-config| *lsp-config*
Configurations for LSP clients is done via |vim.lsp.config()|.
When an LSP client starts, it resolves a configuration by merging
configurations, in increasing priority, from the following:
1. Configuration defined for the `'*'` name.
2. Configuration from the result of sourcing all `lsp/<name>.lua` files
in 'runtimepath' for a server of name `name`.
Note: because of this, calls to |vim.lsp.config()| in `lsp/*.lua` are
treated independently to other calls. This ensures configurations
defined in `lsp/*.lua` have a lower priority.
3. Configurations defined anywhere else.
Note: The merge semantics of configurations follow the behaviour of
|vim.tbl_deep_extend()|.
Example:
Given: >lua
-- Defined in init.lua
vim.lsp.config('*', {
capabilities = {
textDocument = {
semanticTokens = {
multilineTokenSupport = true,
}
}
}
root_markers = { '.git' },
})
-- Defined in ../lsp/clangd.lua
vim.lsp.config('clangd', {
cmd = { 'clangd' },
root_markers = { '.clangd', 'compile_commands.json' },
filetypes = { 'c', 'cpp' },
})
-- Defined in init.lua
vim.lsp.config('clangd', {
filetypes = { 'c' },
})
<
Results in the configuration: >lua
{
-- From the clangd configuration in <rtp>/lsp/clangd.lua
cmd = { 'clangd' },
-- From the clangd configuration in <rtp>/lsp/clangd.lua
-- Overrides the * configuration in init.lua
root_markers = { '.clangd', 'compile_commands.json' },
-- From the clangd configuration in init.lua
-- Overrides the * configuration in init.lua
filetypes = { 'c' },
-- From the * configuration in init.lua
capabilities = {
textDocument = {
semanticTokens = {
multilineTokenSupport = true,
}
}
}
}
<
*lsp-defaults* *lsp-defaults*
When the Nvim LSP client starts it enables diagnostics |vim.diagnostic| (see When the Nvim LSP client starts it enables diagnostics |vim.diagnostic| (see
|vim.diagnostic.config()| to customize). It also sets various default options, |vim.diagnostic.config()| to customize). It also sets various default options,
@ -98,7 +181,7 @@ To override or delete any of the above defaults, set or unset the options on
end, end,
}) })
< <
*lsp-config* *lsp-attach*
To use other LSP features, set keymaps and other buffer options on To use other LSP features, set keymaps and other buffer options on
|LspAttach|. Not all language servers provide the same capabilities. Use |LspAttach|. Not all language servers provide the same capabilities. Use
capability checks to ensure you only use features supported by the language capability checks to ensure you only use features supported by the language
@ -107,16 +190,16 @@ server. Example: >lua
vim.api.nvim_create_autocmd('LspAttach', { vim.api.nvim_create_autocmd('LspAttach', {
callback = function(args) callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id) local client = vim.lsp.get_client_by_id(args.data.client_id)
if client.supports_method('textDocument/implementation') then if client:supports_method('textDocument/implementation') then
-- Create a keymap for vim.lsp.buf.implementation -- Create a keymap for vim.lsp.buf.implementation
end end
if client.supports_method('textDocument/completion') then if client:supports_method('textDocument/completion') then
-- Enable auto-completion -- Enable auto-completion
vim.lsp.completion.enable(true, client.id, args.buf, {autotrigger = true}) vim.lsp.completion.enable(true, client.id, args.buf, {autotrigger = true})
end end
if client.supports_method('textDocument/formatting') then if client:supports_method('textDocument/formatting') then
-- Format the current buffer on save -- Format the current buffer on save
vim.api.nvim_create_autocmd('BufWritePre', { vim.api.nvim_create_autocmd('BufWritePre', {
buffer = args.buf, buffer = args.buf,
@ -465,7 +548,7 @@ EVENTS *lsp-events*
LspAttach *LspAttach* LspAttach *LspAttach*
After an LSP client attaches to a buffer. The |autocmd-pattern| is the After an LSP client attaches to a buffer. The |autocmd-pattern| is the
name of the buffer. When used from Lua, the client ID is passed to the name of the buffer. When used from Lua, the client ID is passed to the
callback in the "data" table. See |lsp-config| for an example. callback in the "data" table. See |lsp-attach| for an example.
LspDetach *LspDetach* LspDetach *LspDetach*
Just before an LSP client detaches from a buffer. The |autocmd-pattern| Just before an LSP client detaches from a buffer. The |autocmd-pattern|
@ -478,7 +561,7 @@ LspDetach *LspDetach*
local client = vim.lsp.get_client_by_id(args.data.client_id) local client = vim.lsp.get_client_by_id(args.data.client_id)
-- Remove the autocommand to format the buffer on save, if it exists -- Remove the autocommand to format the buffer on save, if it exists
if client.supports_method('textDocument/formatting') then if client:supports_method('textDocument/formatting') then
vim.api.nvim_clear_autocmds({ vim.api.nvim_clear_autocmds({
event = 'BufWritePre', event = 'BufWritePre',
buffer = args.buf, buffer = args.buf,
@ -590,6 +673,27 @@ LspTokenUpdate *LspTokenUpdate*
============================================================================== ==============================================================================
Lua module: vim.lsp *lsp-core* Lua module: vim.lsp *lsp-core*
*vim.lsp.Config*
Extends: |vim.lsp.ClientConfig|
Fields: ~
• {cmd}? (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient`)
See `cmd` in |vim.lsp.ClientConfig|.
• {filetypes}? (`string[]`) Filetypes the client will attach to, if
activated by `vim.lsp.enable()`. If not provided,
then the client will attach to all filetypes.
• {root_markers}? (`string[]`) Directory markers (.e.g. '.git/') where
the LSP server will base its workspaceFolders,
rootUri, and rootPath on initialization. Unused if
`root_dir` is provided.
• {reuse_client}? (`fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean`)
Predicate used to decide if a client should be
re-used. Used on all running clients. The default
implementation re-uses a client if name and root_dir
matches.
buf_attach_client({bufnr}, {client_id}) *vim.lsp.buf_attach_client()* buf_attach_client({bufnr}, {client_id}) *vim.lsp.buf_attach_client()*
Implements the `textDocument/did…` notifications required to track a Implements the `textDocument/did…` notifications required to track a
buffer for any language server. buffer for any language server.
@ -689,7 +793,7 @@ commands *vim.lsp.commands*
value is a function which is called if any LSP action (code action, code value is a function which is called if any LSP action (code action, code
lenses, ...) triggers the command. lenses, ...) triggers the command.
If a LSP response contains a command for which no matching entry is If an LSP response contains a command for which no matching entry is
available in this registry, the command will be executed via the LSP available in this registry, the command will be executed via the LSP
server using `workspace/executeCommand`. server using `workspace/executeCommand`.
@ -698,6 +802,65 @@ commands *vim.lsp.commands*
The second argument is the `ctx` of |lsp-handler| The second argument is the `ctx` of |lsp-handler|
config({name}, {cfg}) *vim.lsp.config()*
Update the configuration for an LSP client.
Use name '*' to set default configuration for all clients.
Can also be table-assigned to redefine the configuration for a client.
Examples:
• Add a root marker for all clients: >lua
vim.lsp.config('*', {
root_markers = { '.git' },
})
<
• Add additional capabilities to all clients: >lua
vim.lsp.config('*', {
capabilities = {
textDocument = {
semanticTokens = {
multilineTokenSupport = true,
}
}
}
})
<
• (Re-)define the configuration for clangd: >lua
vim.lsp.config.clangd = {
cmd = {
'clangd',
'--clang-tidy',
'--background-index',
'--offset-encoding=utf-8',
},
root_markers = { '.clangd', 'compile_commands.json' },
filetypes = { 'c', 'cpp' },
}
<
• Get configuration for luals: >lua
local cfg = vim.lsp.config.luals
<
Parameters: ~
• {name} (`string`)
• {cfg} (`vim.lsp.Config`) See |vim.lsp.Config|.
enable({name}, {enable}) *vim.lsp.enable()*
Enable an LSP server to automatically start when opening a buffer.
Uses configuration defined with `vim.lsp.config`.
Examples: >lua
vim.lsp.enable('clangd')
vim.lsp.enable({'luals', 'pyright'})
<
Parameters: ~
• {name} (`string|string[]`) Name(s) of client(s) to enable.
• {enable} (`boolean?`) `true|nil` to enable, `false` to disable.
foldclose({kind}, {winid}) *vim.lsp.foldclose()* foldclose({kind}, {winid}) *vim.lsp.foldclose()*
Close all {kind} of folds in the the window with {winid}. Close all {kind} of folds in the the window with {winid}.

View File

@ -237,6 +237,9 @@ LSP
• Functions in |vim.lsp.Client| can now be called as methods. • Functions in |vim.lsp.Client| can now be called as methods.
• Implemented LSP folding: |vim.lsp.foldexpr()| • Implemented LSP folding: |vim.lsp.foldexpr()|
https://microsoft.github.io/language-server-protocol/specification/#textDocument_foldingRange https://microsoft.github.io/language-server-protocol/specification/#textDocument_foldingRange
• |vim.lsp.config()| has been added to define default configurations for
servers. In addition, configurations can be specified in `lsp/<name>.lua`.
• |vim.lsp.enable()| has been added to enable servers.
LUA LUA

View File

@ -4810,6 +4810,7 @@ A jump table for the options with a short description can be found at |Q_op|.
indent/ indent scripts |indent-expression| indent/ indent scripts |indent-expression|
keymap/ key mapping files |mbyte-keymap| keymap/ key mapping files |mbyte-keymap|
lang/ menu translations |:menutrans| lang/ menu translations |:menutrans|
lsp/ LSP client configurations |lsp-config|
lua/ |Lua| plugins lua/ |Lua| plugins
menu.vim GUI menus |menu.vim| menu.vim GUI menus |menu.vim|
pack/ packages |:packadd| pack/ packages |:packadd|

View File

@ -5010,6 +5010,7 @@ vim.go.ruf = vim.go.rulerformat
--- indent/ indent scripts `indent-expression` --- indent/ indent scripts `indent-expression`
--- keymap/ key mapping files `mbyte-keymap` --- keymap/ key mapping files `mbyte-keymap`
--- lang/ menu translations `:menutrans` --- lang/ menu translations `:menutrans`
--- lsp/ LSP client configurations `lsp-config`
--- lua/ `Lua` plugins --- lua/ `Lua` plugins
--- menu.vim GUI menus `menu.vim` --- menu.vim GUI menus `menu.vim`
--- pack/ packages `:packadd` --- pack/ packages `:packadd`

View File

@ -316,6 +316,240 @@ local function create_and_initialize_client(config)
return client.id, nil return client.id, nil
end end
--- @class vim.lsp.Config : vim.lsp.ClientConfig
---
--- See `cmd` in [vim.lsp.ClientConfig].
--- @field cmd? string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
---
--- Filetypes the client will attach to, if activated by `vim.lsp.enable()`.
--- If not provided, then the client will attach to all filetypes.
--- @field filetypes? string[]
---
--- Directory markers (.e.g. '.git/') where the LSP server will base its workspaceFolders,
--- rootUri, and rootPath on initialization. Unused if `root_dir` is provided.
--- @field root_markers? string[]
---
--- Predicate used to decide if a client should be re-used. Used on all
--- running clients. The default implementation re-uses a client if name and
--- root_dir matches.
--- @field reuse_client? fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean
--- Update the configuration for an LSP client.
---
--- Use name '*' to set default configuration for all clients.
---
--- Can also be table-assigned to redefine the configuration for a client.
---
--- Examples:
---
--- - Add a root marker for all clients:
--- ```lua
--- vim.lsp.config('*', {
--- root_markers = { '.git' },
--- })
--- ```
--- - Add additional capabilities to all clients:
--- ```lua
--- vim.lsp.config('*', {
--- capabilities = {
--- textDocument = {
--- semanticTokens = {
--- multilineTokenSupport = true,
--- }
--- }
--- }
--- })
--- ```
--- - (Re-)define the configuration for clangd:
--- ```lua
--- vim.lsp.config.clangd = {
--- cmd = {
--- 'clangd',
--- '--clang-tidy',
--- '--background-index',
--- '--offset-encoding=utf-8',
--- },
--- root_markers = { '.clangd', 'compile_commands.json' },
--- filetypes = { 'c', 'cpp' },
--- }
--- ```
--- - Get configuration for luals:
--- ```lua
--- local cfg = vim.lsp.config.luals
--- ```
---
--- @param name string
--- @param cfg vim.lsp.Config
--- @diagnostic disable-next-line:assign-type-mismatch
function lsp.config(name, cfg)
local _, _ = name, cfg -- ignore unused
-- dummy proto for docs
end
lsp._enabled_configs = {} --- @type table<string,{resolved_config:vim.lsp.Config?}>
--- If a config in vim.lsp.config() is accessed then the resolved config becomes invalid.
--- @param name string
local function invalidate_enabled_config(name)
if name == '*' then
for _, v in pairs(lsp._enabled_configs) do
v.resolved_config = nil
end
elseif lsp._enabled_configs[name] then
lsp._enabled_configs[name].resolved_config = nil
end
end
--- @nodoc
--- @class vim.lsp.config
--- @field [string] vim.lsp.Config
--- @field package _configs table<string,vim.lsp.Config>
lsp.config = setmetatable({ _configs = {} }, {
--- @param self vim.lsp.config
--- @param name string
--- @return vim.lsp.Config
__index = function(self, name)
validate('name', name, 'string')
invalidate_enabled_config(name)
self._configs[name] = self._configs[name] or {}
return self._configs[name]
end,
--- @param self vim.lsp.config
--- @param name string
--- @param cfg vim.lsp.Config
__newindex = function(self, name, cfg)
validate('name', name, 'string')
validate('cfg', cfg, 'table')
invalidate_enabled_config(name)
self._configs[name] = cfg
end,
--- @param self vim.lsp.config
--- @param name string
--- @param cfg vim.lsp.Config
__call = function(self, name, cfg)
validate('name', name, 'string')
validate('cfg', cfg, 'table')
invalidate_enabled_config(name)
self[name] = vim.tbl_deep_extend('force', self._configs[name] or {}, cfg)
end,
})
--- @private
--- @param name string
--- @return vim.lsp.Config
function lsp._resolve_config(name)
local econfig = lsp._enabled_configs[name] or {}
if not econfig.resolved_config then
-- Resolve configs from lsp/*.lua
-- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites.
local orig_configs = lsp.config._configs
lsp.config._configs = {}
pcall(vim.cmd.runtime, { ('lsp/%s.lua'):format(name), bang = true })
local rtp_configs = lsp.config._configs
lsp.config._configs = orig_configs
local config = vim.tbl_deep_extend(
'force',
lsp.config._configs['*'] or {},
rtp_configs[name] or {},
lsp.config._configs[name] or {}
)
config.name = name
validate('cmd', config.cmd, { 'function', 'table' })
validate('cmd', config.reuse_client, 'function', true)
-- All other fields are validated in client.create
econfig.resolved_config = config
end
return assert(econfig.resolved_config)
end
local lsp_enable_autocmd_id --- @type integer?
--- @param bufnr integer
local function lsp_enable_callback(bufnr)
-- Only ever attach to buffers that represent an actual file.
if vim.bo[bufnr].buftype ~= '' then
return
end
--- @param config vim.lsp.Config
local function can_start(config)
if config.filetypes and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) then
return false
elseif type(config.cmd) == 'table' and vim.fn.executable(config.cmd[1]) == 0 then
return false
end
return true
end
for name in vim.spairs(lsp._enabled_configs) do
local config = lsp._resolve_config(name)
if can_start(config) then
-- Deepcopy config so changes done in the client
-- do not propagate back to the enabled configs.
config = vim.deepcopy(config)
vim.lsp.start(config, {
bufnr = bufnr,
reuse_client = config.reuse_client,
_root_markers = config.root_markers,
})
end
end
end
--- Enable an LSP server to automatically start when opening a buffer.
---
--- Uses configuration defined with `vim.lsp.config`.
---
--- Examples:
---
--- ```lua
--- vim.lsp.enable('clangd')
---
--- vim.lsp.enable({'luals', 'pyright'})
--- ```
---
--- @param name string|string[] Name(s) of client(s) to enable.
--- @param enable? boolean `true|nil` to enable, `false` to disable.
function lsp.enable(name, enable)
validate('name', name, { 'string', 'table' })
local names = vim._ensure_list(name) --[[@as string[] ]]
for _, nm in ipairs(names) do
if nm == '*' then
error('Invalid name')
end
lsp._enabled_configs[nm] = enable == false and nil or {}
end
if not next(lsp._enabled_configs) then
if lsp_enable_autocmd_id then
api.nvim_del_autocmd(lsp_enable_autocmd_id)
lsp_enable_autocmd_id = nil
end
return
end
-- Only ever create autocmd once to reuse computation of config merging.
lsp_enable_autocmd_id = lsp_enable_autocmd_id
or api.nvim_create_autocmd('FileType', {
group = api.nvim_create_augroup('nvim.lsp.enable', {}),
callback = function(args)
lsp_enable_callback(args.buf)
end,
})
end
--- @class vim.lsp.start.Opts --- @class vim.lsp.start.Opts
--- @inlinedoc --- @inlinedoc
--- ---
@ -334,6 +568,8 @@ end
--- ---
--- Suppress error reporting if the LSP server fails to start (default false). --- Suppress error reporting if the LSP server fails to start (default false).
--- @field silent? boolean --- @field silent? boolean
---
--- @field package _root_markers? string[]
--- Create a new LSP client and start a language server or reuses an already --- Create a new LSP client and start a language server or reuses an already
--- running client if one is found matching `name` and `root_dir`. --- running client if one is found matching `name` and `root_dir`.
@ -379,6 +615,11 @@ function lsp.start(config, opts)
local reuse_client = opts.reuse_client or reuse_client_default local reuse_client = opts.reuse_client or reuse_client_default
local bufnr = vim._resolve_bufnr(opts.bufnr) local bufnr = vim._resolve_bufnr(opts.bufnr)
if not config.root_dir and opts._root_markers then
config = vim.deepcopy(config)
config.root_dir = vim.fs.root(bufnr, opts._root_markers)
end
for _, client in pairs(all_clients) do for _, client in pairs(all_clients) do
if reuse_client(client, config) then if reuse_client(client, config) then
if opts.attach == false then if opts.attach == false then
@ -387,9 +628,8 @@ function lsp.start(config, opts)
if lsp.buf_attach_client(bufnr, client.id) then if lsp.buf_attach_client(bufnr, client.id) then
return client.id return client.id
else
return nil
end end
return
end end
end end
@ -398,7 +638,7 @@ function lsp.start(config, opts)
if not opts.silent then if not opts.silent then
vim.notify(err, vim.log.levels.WARN) vim.notify(err, vim.log.levels.WARN)
end end
return nil return
end end
if opts.attach == false then if opts.attach == false then
@ -408,8 +648,6 @@ function lsp.start(config, opts)
if client_id and lsp.buf_attach_client(bufnr, client_id) then if client_id and lsp.buf_attach_client(bufnr, client_id) then
return client_id return client_id
end end
return nil
end end
--- Consumes the latest progress messages from all clients and formats them as a string. --- Consumes the latest progress messages from all clients and formats them as a string.
@ -1275,7 +1513,7 @@ end
--- and the value is a function which is called if any LSP action --- and the value is a function which is called if any LSP action
--- (code action, code lenses, ...) triggers the command. --- (code action, code lenses, ...) triggers the command.
--- ---
--- If a LSP response contains a command for which no matching entry is --- If an LSP response contains a command for which no matching entry is
--- available in this registry, the command will be executed via the LSP server --- available in this registry, the command will be executed via the LSP server
--- using `workspace/executeCommand`. --- using `workspace/executeCommand`.
--- ---

View File

@ -359,16 +359,6 @@ local function get_name(id, config)
return tostring(id) return tostring(id)
end end
--- @generic T
--- @param x elem_or_list<T>?
--- @return T[]
local function ensure_list(x)
if type(x) == 'table' then
return x
end
return { x }
end
--- @nodoc --- @nodoc
--- @param config vim.lsp.ClientConfig --- @param config vim.lsp.ClientConfig
--- @return vim.lsp.Client? --- @return vim.lsp.Client?
@ -395,13 +385,13 @@ function Client.create(config)
settings = config.settings or {}, settings = config.settings or {},
flags = config.flags or {}, flags = config.flags or {},
get_language_id = config.get_language_id or default_get_language_id, get_language_id = config.get_language_id or default_get_language_id,
capabilities = config.capabilities or lsp.protocol.make_client_capabilities(), capabilities = config.capabilities,
workspace_folders = lsp._get_workspace_folders(config.workspace_folders or config.root_dir), workspace_folders = lsp._get_workspace_folders(config.workspace_folders or config.root_dir),
root_dir = config.root_dir, root_dir = config.root_dir,
_before_init_cb = config.before_init, _before_init_cb = config.before_init,
_on_init_cbs = ensure_list(config.on_init), _on_init_cbs = vim._ensure_list(config.on_init),
_on_exit_cbs = ensure_list(config.on_exit), _on_exit_cbs = vim._ensure_list(config.on_exit),
_on_attach_cbs = ensure_list(config.on_attach), _on_attach_cbs = vim._ensure_list(config.on_attach),
_on_error_cb = config.on_error, _on_error_cb = config.on_error,
_trace = get_trace(config.trace), _trace = get_trace(config.trace),
@ -417,6 +407,9 @@ function Client.create(config)
messages = { name = name, messages = {}, progress = {}, status = {} }, messages = { name = name, messages = {}, progress = {}, status = {} },
} }
self.capabilities =
vim.tbl_deep_extend('force', lsp.protocol.make_client_capabilities(), self.capabilities or {})
--- @class lsp.DynamicCapabilities --- @class lsp.DynamicCapabilities
--- @nodoc --- @nodoc
self.dynamic_capabilities = { self.dynamic_capabilities = {

View File

@ -28,42 +28,45 @@ local function check_log()
report_fn(string.format('Log size: %d KB', log_size / 1000)) report_fn(string.format('Log size: %d KB', log_size / 1000))
end end
--- @param f function
--- @return string
local function func_tostring(f)
local info = debug.getinfo(f, 'S')
return ('<function %s:%s>'):format(info.source, info.linedefined)
end
local function check_active_clients() local function check_active_clients()
vim.health.start('vim.lsp: Active Clients') vim.health.start('vim.lsp: Active Clients')
local clients = vim.lsp.get_clients() local clients = vim.lsp.get_clients()
if next(clients) then if next(clients) then
for _, client in pairs(clients) do for _, client in pairs(clients) do
local cmd ---@type string local cmd ---@type string
if type(client.config.cmd) == 'table' then local ccmd = client.config.cmd
cmd = table.concat(client.config.cmd --[[@as table]], ' ') if type(ccmd) == 'table' then
elseif type(client.config.cmd) == 'function' then cmd = vim.inspect(ccmd)
cmd = tostring(client.config.cmd) elseif type(ccmd) == 'function' then
cmd = func_tostring(ccmd)
end end
local dirs_info ---@type string local dirs_info ---@type string
if client.workspace_folders and #client.workspace_folders > 1 then if client.workspace_folders and #client.workspace_folders > 1 then
dirs_info = string.format( local wfolders = {} --- @type string[]
' Workspace folders:\n %s', for _, dir in ipairs(client.workspace_folders) do
vim wfolders[#wfolders + 1] = dir.name
.iter(client.workspace_folders) end
---@param folder lsp.WorkspaceFolder dirs_info = ('- Workspace folders:\n %s'):format(table.concat(wfolders, '\n '))
:map(function(folder)
return folder.name
end)
:join('\n ')
)
else else
dirs_info = string.format( dirs_info = string.format(
' Root directory: %s', '- Root directory: %s',
client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~') client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~')
) or nil ) or nil
end end
report_info(table.concat({ report_info(table.concat({
string.format('%s (id: %d)', client.name, client.id), string.format('%s (id: %d)', client.name, client.id),
dirs_info, dirs_info,
string.format(' Command: %s', cmd), string.format('- Command: %s', cmd),
string.format(' Settings: %s', vim.inspect(client.settings, { newline = '\n ' })), string.format('- Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
string.format( string.format(
' Attached buffers: %s', '- Attached buffers: %s',
vim.iter(pairs(client.attached_buffers)):map(tostring):join(', ') vim.iter(pairs(client.attached_buffers)):map(tostring):join(', ')
), ),
}, '\n')) }, '\n'))
@ -174,10 +177,45 @@ local function check_position_encodings()
end end
end end
local function check_enabled_configs()
vim.health.start('vim.lsp: Enabled Configurations')
for name in vim.spairs(vim.lsp._enabled_configs) do
local config = vim.lsp._resolve_config(name)
local text = {} --- @type string[]
text[#text + 1] = ('%s:'):format(name)
for k, v in
vim.spairs(config --[[@as table<string,any>]])
do
local v_str --- @type string?
if k == 'name' then
v_str = nil
elseif k == 'filetypes' or k == 'root_markers' then
v_str = table.concat(v, ', ')
elseif type(v) == 'function' then
v_str = func_tostring(v)
else
v_str = vim.inspect(v, { newline = '\n ' })
end
if k == 'cmd' and type(v) == 'table' and vim.fn.executable(v[1]) == 0 then
report_warn(("'%s' is not executable. Configuration will not be used."):format(v[1]))
end
if v_str then
text[#text + 1] = ('- %s: %s'):format(k, v_str)
end
end
text[#text + 1] = ''
report_info(table.concat(text, '\n'))
end
end
--- Performs a healthcheck for LSP --- Performs a healthcheck for LSP
function M.check() function M.check()
check_log() check_log()
check_active_clients() check_active_clients()
check_enabled_configs()
check_watcher() check_watcher()
check_position_encodings() check_position_encodings()
end end

View File

@ -1409,4 +1409,14 @@ function vim._resolve_bufnr(bufnr)
return bufnr return bufnr
end end
--- @generic T
--- @param x elem_or_list<T>?
--- @return T[]
function vim._ensure_list(x)
if type(x) == 'table' then
return x
end
return { x }
end
return vim return vim

View File

@ -515,6 +515,8 @@ local function inline_type(obj, classes)
elseif desc == '' then elseif desc == '' then
if ty_islist then if ty_islist then
desc = desc .. 'A list of objects with the following fields:' desc = desc .. 'A list of objects with the following fields:'
elseif cls.parent then
desc = desc .. fmt('Extends |%s| with the additional fields:', cls.parent)
else else
desc = desc .. 'A table with the following fields:' desc = desc .. 'A table with the following fields:'
end end

View File

@ -6755,6 +6755,7 @@ return {
indent/ indent scripts |indent-expression| indent/ indent scripts |indent-expression|
keymap/ key mapping files |mbyte-keymap| keymap/ key mapping files |mbyte-keymap|
lang/ menu translations |:menutrans| lang/ menu translations |:menutrans|
lsp/ LSP client configurations |lsp-config|
lua/ |Lua| plugins lua/ |Lua| plugins
menu.vim GUI menus |menu.vim| menu.vim GUI menus |menu.vim|
pack/ packages |:packadd| pack/ packages |:packadd|

View File

@ -6098,15 +6098,6 @@ describe('LSP', function()
end end
eq(is_os('mac') or is_os('win'), check_registered(nil)) -- start{_client}() defaults to make_client_capabilities(). eq(is_os('mac') or is_os('win'), check_registered(nil)) -- start{_client}() defaults to make_client_capabilities().
eq(false, check_registered(vim.empty_dict()))
eq(
false,
check_registered({
workspace = {
ignoreMe = true,
},
})
)
eq( eq(
false, false,
check_registered({ check_registered({
@ -6129,4 +6120,88 @@ describe('LSP', function()
) )
end) end)
end) end)
describe('vim.lsp.config() and vim.lsp.enable()', function()
it('can merge settings from "*"', function()
eq(
{
name = 'foo',
cmd = { 'foo' },
root_markers = { '.git' },
},
exec_lua(function()
vim.lsp.config('*', { root_markers = { '.git' } })
vim.lsp.config('foo', { cmd = { 'foo' } })
return vim.lsp._resolve_config('foo')
end)
)
end)
it('sets up an autocmd', function()
eq(
1,
exec_lua(function()
vim.lsp.config('foo', {
cmd = { 'foo' },
root_markers = { '.foorc' },
})
vim.lsp.enable('foo')
return #vim.api.nvim_get_autocmds({
group = 'nvim.lsp.enable',
event = 'FileType',
})
end)
)
end)
it('attaches to buffers', function()
exec_lua(create_server_definition)
local tmp1 = t.tmpname(true)
local tmp2 = t.tmpname(true)
exec_lua(function()
local server = _G._create_server({
handlers = {
initialize = function(_, _, callback)
callback(nil, { capabilities = {} })
end,
},
})
vim.lsp.config('foo', {
cmd = server.cmd,
filetypes = { 'foo' },
root_markers = { '.foorc' },
})
vim.lsp.config('bar', {
cmd = server.cmd,
filetypes = { 'bar' },
root_markers = { '.foorc' },
})
vim.lsp.enable('foo')
vim.lsp.enable('bar')
vim.cmd.edit(tmp1)
vim.bo.filetype = 'foo'
_G.foo_buf = vim.api.nvim_get_current_buf()
vim.cmd.edit(tmp2)
vim.bo.filetype = 'bar'
_G.bar_buf = vim.api.nvim_get_current_buf()
end)
eq(
{ 1, 'foo', 1, 'bar' },
exec_lua(function()
local foos = vim.lsp.get_clients({ bufnr = assert(_G.foo_buf) })
local bars = vim.lsp.get_clients({ bufnr = assert(_G.bar_buf) })
return { #foos, foos[1].name, #bars, bars[1].name }
end)
)
end)
end)
end) end)