aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/lsp.txt221
-rw-r--r--runtime/doc/news.txt3
-rw-r--r--runtime/doc/options.txt1
-rw-r--r--runtime/lua/vim/_meta/options.lua1
-rw-r--r--runtime/lua/vim/lsp.lua250
-rw-r--r--runtime/lua/vim/lsp/client.lua21
-rw-r--r--runtime/lua/vim/lsp/health.lua74
-rw-r--r--runtime/lua/vim/shared.lua10
-rwxr-xr-xscripts/gen_vimdoc.lua2
-rw-r--r--src/nvim/options.lua1
-rw-r--r--test/functional/plugin/lsp_spec.lua93
11 files changed, 601 insertions, 76 deletions
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
index d9e536b79b..64145ebf11 100644
--- a/runtime/doc/lsp.txt
+++ b/runtime/doc/lsp.txt
@@ -28,31 +28,114 @@ Follow these steps to get LSP features:
upstream installation instructions. You can find language servers here:
https://microsoft.github.io/language-server-protocol/implementors/servers/
-2. Use |vim.lsp.start()| to start the LSP server (or attach to an existing
- one) when a file is opened. Example: >lua
- -- Create an event handler for the FileType autocommand
- vim.api.nvim_create_autocmd('FileType', {
- -- This handler will fire when the buffer's 'filetype' is "python"
- 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
- -- current buffer (`args.buf`) that contains either a "setup.py" or a
- -- "pyproject.toml" file. Files that share a root directory will reuse
- -- the connection to the same LSP server.
- root_dir = vim.fs.root(args.buf, {'setup.py', 'pyproject.toml'}),
- })
- end,
- })
+2. Use |vim.lsp.config()| to define a configuration for an LSP client.
+ Example: >lua
+ vim.lsp.config['luals'] = {
+ -- Command and arguments to start the server.
+ cmd = { 'lua-language-server' }
+
+ -- Filetypes to automatically attach to.
+ filetypes = { 'lua' },
+
+ -- Sets the "root directory" to the parent directory of the file in the
+ -- current buffer that contains either a ".luarc.json" or a
+ -- ".luarc.jsonc" file. Files that share a root directory will reuse
+ -- 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
- :checkhealth lsp
+3. Use |vim.lsp.enable()| to enable a configuration.
+ 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|
+
+ *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:
-4. (Optional) Configure keymaps and autocommands to use LSP features. |lsp-config|
+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*
When the Nvim LSP client starts it enables diagnostics |vim.diagnostic| (see
|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,
})
<
- *lsp-config*
+ *lsp-attach*
To use other LSP features, set keymaps and other buffer options on
|LspAttach|. Not all language servers provide the same capabilities. Use
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', {
callback = function(args)
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
end
- if client.supports_method('textDocument/completion') then
+ if client:supports_method('textDocument/completion') then
-- Enable auto-completion
vim.lsp.completion.enable(true, client.id, args.buf, {autotrigger = true})
end
- if client.supports_method('textDocument/formatting') then
+ if client:supports_method('textDocument/formatting') then
-- Format the current buffer on save
vim.api.nvim_create_autocmd('BufWritePre', {
buffer = args.buf,
@@ -465,7 +548,7 @@ EVENTS *lsp-events*
LspAttach *LspAttach*
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
- callback in the "data" table. See |lsp-config| for an example.
+ callback in the "data" table. See |lsp-attach| for an example.
LspDetach *LspDetach*
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)
-- 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({
event = 'BufWritePre',
buffer = args.buf,
@@ -590,6 +673,27 @@ LspTokenUpdate *LspTokenUpdate*
==============================================================================
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()*
Implements the `textDocument/did…` notifications required to track a
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
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 using `workspace/executeCommand`.
@@ -698,6 +802,65 @@ commands *vim.lsp.commands*
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()*
Close all {kind} of folds in the the window with {winid}.
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index ef055161df..71ec84c2f2 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -237,6 +237,9 @@ LSP
• Functions in |vim.lsp.Client| can now be called as methods.
• Implemented LSP folding: |vim.lsp.foldexpr()|
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
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 5763d16cad..6fe208f506 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -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|
keymap/ key mapping files |mbyte-keymap|
lang/ menu translations |:menutrans|
+ lsp/ LSP client configurations |lsp-config|
lua/ |Lua| plugins
menu.vim GUI menus |menu.vim|
pack/ packages |:packadd|
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
index 7a8c3a6c29..fecbece655 100644
--- a/runtime/lua/vim/_meta/options.lua
+++ b/runtime/lua/vim/_meta/options.lua
@@ -5010,6 +5010,7 @@ vim.go.ruf = vim.go.rulerformat
--- indent/ indent scripts `indent-expression`
--- keymap/ key mapping files `mbyte-keymap`
--- lang/ menu translations `:menutrans`
+--- lsp/ LSP client configurations `lsp-config`
--- lua/ `Lua` plugins
--- menu.vim GUI menus `menu.vim`
--- pack/ packages `:packadd`
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index ebdc050405..596e1b609b 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -316,6 +316,240 @@ local function create_and_initialize_client(config)
return client.id, nil
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
--- @inlinedoc
---
@@ -334,6 +568,8 @@ end
---
--- Suppress error reporting if the LSP server fails to start (default false).
--- @field silent? boolean
+---
+--- @field package _root_markers? string[]
--- 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`.
@@ -379,6 +615,11 @@ function lsp.start(config, opts)
local reuse_client = opts.reuse_client or reuse_client_default
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
if reuse_client(client, config) then
if opts.attach == false then
@@ -387,9 +628,8 @@ function lsp.start(config, opts)
if lsp.buf_attach_client(bufnr, client.id) then
return client.id
- else
- return nil
end
+ return
end
end
@@ -398,7 +638,7 @@ function lsp.start(config, opts)
if not opts.silent then
vim.notify(err, vim.log.levels.WARN)
end
- return nil
+ return
end
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
return client_id
end
-
- return nil
end
--- 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
--- (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
--- using `workspace/executeCommand`.
---
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index 5eefe4600e..72043c18dd 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -359,16 +359,6 @@ local function get_name(id, config)
return tostring(id)
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
--- @param config vim.lsp.ClientConfig
--- @return vim.lsp.Client?
@@ -395,13 +385,13 @@ function Client.create(config)
settings = config.settings or {},
flags = config.flags or {},
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),
root_dir = config.root_dir,
_before_init_cb = config.before_init,
- _on_init_cbs = ensure_list(config.on_init),
- _on_exit_cbs = ensure_list(config.on_exit),
- _on_attach_cbs = ensure_list(config.on_attach),
+ _on_init_cbs = vim._ensure_list(config.on_init),
+ _on_exit_cbs = vim._ensure_list(config.on_exit),
+ _on_attach_cbs = vim._ensure_list(config.on_attach),
_on_error_cb = config.on_error,
_trace = get_trace(config.trace),
@@ -417,6 +407,9 @@ function Client.create(config)
messages = { name = name, messages = {}, progress = {}, status = {} },
}
+ self.capabilities =
+ vim.tbl_deep_extend('force', lsp.protocol.make_client_capabilities(), self.capabilities or {})
+
--- @class lsp.DynamicCapabilities
--- @nodoc
self.dynamic_capabilities = {
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index 0d314108fe..d2cf888d89 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -28,42 +28,45 @@ local function check_log()
report_fn(string.format('Log size: %d KB', log_size / 1000))
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()
vim.health.start('vim.lsp: Active Clients')
local clients = vim.lsp.get_clients()
if next(clients) then
for _, client in pairs(clients) do
local cmd ---@type string
- if type(client.config.cmd) == 'table' then
- cmd = table.concat(client.config.cmd --[[@as table]], ' ')
- elseif type(client.config.cmd) == 'function' then
- cmd = tostring(client.config.cmd)
+ local ccmd = client.config.cmd
+ if type(ccmd) == 'table' then
+ cmd = vim.inspect(ccmd)
+ elseif type(ccmd) == 'function' then
+ cmd = func_tostring(ccmd)
end
local dirs_info ---@type string
if client.workspace_folders and #client.workspace_folders > 1 then
- dirs_info = string.format(
- ' Workspace folders:\n %s',
- vim
- .iter(client.workspace_folders)
- ---@param folder lsp.WorkspaceFolder
- :map(function(folder)
- return folder.name
- end)
- :join('\n ')
- )
+ local wfolders = {} --- @type string[]
+ for _, dir in ipairs(client.workspace_folders) do
+ wfolders[#wfolders + 1] = dir.name
+ end
+ dirs_info = ('- Workspace folders:\n %s'):format(table.concat(wfolders, '\n '))
else
dirs_info = string.format(
- ' Root directory: %s',
+ '- Root directory: %s',
client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~')
) or nil
end
report_info(table.concat({
string.format('%s (id: %d)', client.name, client.id),
dirs_info,
- string.format(' Command: %s', cmd),
- string.format(' Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
+ string.format('- Command: %s', cmd),
+ string.format('- Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
string.format(
- ' Attached buffers: %s',
+ '- Attached buffers: %s',
vim.iter(pairs(client.attached_buffers)):map(tostring):join(', ')
),
}, '\n'))
@@ -174,10 +177,45 @@ local function check_position_encodings()
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
function M.check()
check_log()
check_active_clients()
+ check_enabled_configs()
check_watcher()
check_position_encodings()
end
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index 0fe8e99350..24c3f243e5 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -1409,4 +1409,14 @@ function vim._resolve_bufnr(bufnr)
return bufnr
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
diff --git a/scripts/gen_vimdoc.lua b/scripts/gen_vimdoc.lua
index 3f870c561f..34f1dc9e38 100755
--- a/scripts/gen_vimdoc.lua
+++ b/scripts/gen_vimdoc.lua
@@ -515,6 +515,8 @@ local function inline_type(obj, classes)
elseif desc == '' then
if ty_islist then
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
desc = desc .. 'A table with the following fields:'
end
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
index afce4a918b..de152fb315 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -6755,6 +6755,7 @@ return {
indent/ indent scripts |indent-expression|
keymap/ key mapping files |mbyte-keymap|
lang/ menu translations |:menutrans|
+ lsp/ LSP client configurations |lsp-config|
lua/ |Lua| plugins
menu.vim GUI menus |menu.vim|
pack/ packages |:packadd|
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index e735e20ff5..79952cb933 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -6098,15 +6098,6 @@ describe('LSP', function()
end
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(
false,
check_registered({
@@ -6129,4 +6120,88 @@ describe('LSP', function()
)
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)