From 3f1d09bc94d02266d6fa588a2ccd1be1ca084cf7 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 1 Nov 2024 16:31:51 +0000 Subject: [PATCH] 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()`. --- runtime/doc/lsp.txt | 219 ++++++++++++++++++++---- runtime/doc/news.txt | 3 + runtime/doc/options.txt | 1 + runtime/lua/vim/_meta/options.lua | 1 + runtime/lua/vim/lsp.lua | 250 +++++++++++++++++++++++++++- runtime/lua/vim/lsp/client.lua | 21 +-- runtime/lua/vim/lsp/health.lua | 74 ++++++-- runtime/lua/vim/shared.lua | 10 ++ scripts/gen_vimdoc.lua | 2 + src/nvim/options.lua | 1 + test/functional/plugin/lsp_spec.lua | 93 ++++++++++- 11 files changed, 600 insertions(+), 75 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'}, +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' } - -- 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, - }) + -- 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| -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/.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 /lsp/clangd.lua + cmd = { 'clangd' }, + + -- From the clangd configuration in /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/.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 + +--- 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 +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? ---- @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 (''):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]]) + 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? +--- @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)