From e406c4efd6209e093d2d2caff7e3c9a0847ee030 Mon Sep 17 00:00:00 2001 From: Olivia Kinnear Date: Thu, 19 Mar 2026 06:33:34 -0500 Subject: [PATCH] feat(lsp): vim.lsp.get_configs() #37237 Problem: No way to iterate configs. Users need to reach for `vim.lsp.config._configs`, an internal interface. Solution: Provide vim.lsp.get_configs(). Also indirectly improves :lsp enable/disable completion by discarding invalid configs from completion. --- runtime/doc/lsp.txt | 17 +++++++ runtime/doc/news.txt | 1 + runtime/lua/vim/_core/ex_cmd.lua | 56 ++++++----------------- runtime/lua/vim/lsp.lua | 70 +++++++++++++++++++++++++++++ test/functional/plugin/lsp_spec.lua | 46 +++++++++++++++++++ 5 files changed, 147 insertions(+), 43 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index e8e4ce509b..56d89c2178 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -1153,6 +1153,23 @@ get_clients({filter}) *vim.lsp.get_clients()* Return: ~ (`vim.lsp.Client[]`) List of |vim.lsp.Client| objects +get_configs({filter}) *vim.lsp.get_configs()* + Get LSP configs. + + Note: Will eagerly evaluate config files in `'runtimepath'` if necessary. + + Parameters: ~ + • {filter} (`table?`) Key-value pairs used to filter the returned + configs. + • {enabled}? (`boolean`) If true, only return enabled + configs. If false, only return configs that aren't + enabled. + • {filetype}? (`string`) Only return configs which attach to + the given filetype. + + Return: ~ + (`vim.lsp.Config[]`) List of |vim.lsp.Config| objects + is_enabled({name}) *vim.lsp.is_enabled()* Checks if the given LSP config is enabled (globally, not per-buffer). diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 3631d6199c..af2d9893cb 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -330,6 +330,7 @@ LSP • Support for `workspace/codeLens/refresh`: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeLens_refresh • |gx| opens `textDocument/documentLink` items found at cursor. +• |vim.lsp.get_configs()| can get all LSP configs matching certain conditions. LUA diff --git a/runtime/lua/vim/_core/ex_cmd.lua b/runtime/lua/vim/_core/ex_cmd.lua index a0bd3463ab..a46892abaa 100644 --- a/runtime/lua/vim/_core/ex_cmd.lua +++ b/runtime/lua/vim/_core/ex_cmd.lua @@ -19,45 +19,22 @@ local function get_client_names() :totable() end ---- @return string[] -local function get_config_names() - local config_names = vim - .iter(api.nvim_get_runtime_file('lsp/*.lua', true)) - --- @param path string - :map(function(path) - local file_name = path:match('[^/]*.lua$') - return file_name:sub(0, #file_name - 4) - end) - :totable() - - --- @diagnostic disable-next-line - vim.list_extend(config_names, vim.tbl_keys(lsp.config._configs)) - - return vim - .iter(config_names) - :unique() - --- @param name string - :filter(function(name) - return name ~= '*' - end) - :totable() -end - ---- @param filter fun(string):boolean +--- @param filter vim.lsp.get_configs.Filter --- @return fun():string[] local function filtered_config_names(filter) return function() - return vim.iter(get_config_names()):filter(filter):totable() + return vim + .iter(lsp.get_configs(filter)) + :map(function(config) + return config.name + end) + :totable() end end local complete_args = { - enable = filtered_config_names(function(name) - return not lsp.is_enabled(name) - end), - disable = filtered_config_names(function(name) - return lsp.is_enabled(name) - end), + enable = filtered_config_names { enabled = false }, + disable = filtered_config_names { enabled = true }, restart = get_client_names, stop = get_client_names, } @@ -79,17 +56,10 @@ local function ex_lsp_enable(config_names) -- Default to enabling all clients matching the filetype of the current buffer. if #config_names == 0 then local filetype = vim.bo.filetype - for _, name in ipairs(get_config_names()) do - local success, result = pcall(function() - return lsp.config[name] - end) - if success then - local filetypes = result.filetypes - if filetypes == nil or vim.list_contains(filetypes, filetype) then - table.insert(config_names, name) - end - else - echo_err(result --[[@as string]]) + for _, config in ipairs(lsp.get_configs()) do + local filetypes = config.filetypes + if filetypes == nil or vim.list_contains(filetypes, filetype) then + table.insert(config_names, config.name) end end if #config_names == 0 then diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 07af250ef8..3b6f4b4ee2 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -387,6 +387,76 @@ lsp.config = setmetatable({ _configs = {} }, { end, }) +--- @return string[] +local function get_config_names() + local config_names = vim + .iter(api.nvim_get_runtime_file('lsp/*.lua', true)) + --- @param path string + :map(function(path) + local file_name = path:match('[^/]*.lua$') + return file_name:sub(0, #file_name - 4) + end) + :totable() + + vim.list_extend(config_names, vim.tbl_keys(lsp.config._configs)) + + return vim + .iter(config_names) + :unique() + --- @param name string + :filter(function(name) + return name ~= '*' + end) + :totable() +end + +--- Key-value pairs used to filter the returned configs. +--- @class vim.lsp.get_configs.Filter +--- @inlinedoc +--- +--- If true, only return enabled configs. If false, only return configs that +--- aren't enabled. +--- @field enabled? boolean +--- +--- Only return configs which attach to the given filetype. +--- @field filetype? string + +--- Get LSP configs. +--- +--- Note: Will eagerly evaluate config files in `'runtimepath'` if necessary. +--- @param filter? vim.lsp.get_configs.Filter +--- @return vim.lsp.Config[]: List of |vim.lsp.Config| objects +function lsp.get_configs(filter) + validate('filter', filter, 'table', true) + + filter = filter or {} + + local configs = {} --- @type vim.lsp.Config[] + + local config_names --- @type string[] + if not filter.enabled then + config_names = get_config_names() + else + -- Shortcut filtering enabled configs by directly getting enabled configs + config_names = vim.tbl_keys(lsp._enabled_configs) + end + + for _, config_name in ipairs(config_names) do + local config = lsp.config[config_name] + if + config + and (filter.enabled ~= false or not lsp.is_enabled(config_name)) + and ( + filter.filetype == nil + or (config.filetypes ~= nil and vim.list_contains(config.filetypes, filter.filetype)) + ) + then + configs[#configs + 1] = config + end + end + return configs +end + local lsp_enable_autocmd_id --- @type integer? local function validate_cmd(v) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 69da90dd68..5e8d7e9fa6 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -7114,6 +7114,52 @@ describe('LSP', function() exec_lua([[vim.lsp.enable('foo', false)]]) eq(false, exec_lua([[return vim.lsp.is_enabled('foo')]])) end) + + it('vim.lsp.get_configs()', function() + exec_lua(function() + vim.lsp.config('foo', { + cmd = { 'foo' }, + filetypes = { 'foofile' }, + root_markers = { '.foorc' }, + }) + vim.lsp.config('bar', { + cmd = { 'bar' }, + root_markers = { '.barrc' }, + }) + vim.lsp.enable('foo') + end) + + local function names(configs) + local config_names = vim + .iter(configs) + :map(function(config) + return config.name + end) + :totable() + table.sort(config_names) + return config_names + end + + -- With no filter, return all configs + eq({ 'bar', 'foo' }, names(exec_lua([[return vim.lsp.get_configs()]]))) + + -- Confirm `enabled` works + eq({ 'foo' }, names(exec_lua([[return vim.lsp.get_configs { enabled = true }]]))) + eq({ 'bar' }, names(exec_lua([[return vim.lsp.get_configs { enabled = false }]]))) + + -- Confirm `filetype` works + eq({ 'foo' }, names(exec_lua([[return vim.lsp.get_configs { filetype = 'foofile' }]]))) + + -- Confirm filters combine + eq( + { 'foo' }, + names(exec_lua([[return vim.lsp.get_configs { filetype = 'foofile', enabled = true }]])) + ) + eq( + {}, + names(exec_lua([[return vim.lsp.get_configs { filetype = 'foofile', enabled = false }]])) + ) + end) end) describe('vim.lsp.buf.workspace_diagnostics()', function()