backport: feat(lsp): pass resolved config to cmd() #34560

Problem:
In LSP configs, the function form of `cmd()` cannot easily get the
resolved root dir (workspace). One of the main use-cases of a dynamic
`cmd()` is to be able to start a new server  whose binary may be located
*in the workspace* ([example](https://github.com/neovim/nvim-lspconfig/pull/3912)).

Compare `reuse_client()`, which also receives the resolved config.

Solution:
Pass the resolved config to `cmd()`.


(cherry picked from commit 32f30c4874)

Co-authored-by: Julian Visser <12615757+justmejulian@users.noreply.github.com>
This commit is contained in:
Justin M. Keyes
2025-06-18 05:46:53 -07:00
committed by GitHub
parent 6396bfb29f
commit ecf5164d2d
5 changed files with 24 additions and 15 deletions

View File

@ -1309,16 +1309,16 @@ Lua module: vim.lsp.client *lsp-client*
• Note: To send an empty dictionary use • Note: To send an empty dictionary use
|vim.empty_dict()|, else it will be encoded |vim.empty_dict()|, else it will be encoded
as an array. as an array.
• {cmd} (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient`) • {cmd} (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.PublicClient`)
Command `string[]` that launches the language Command `string[]` that launches the language
server (treated as in |jobstart()|, must be server (treated as in |jobstart()|, must be
absolute or on `$PATH`, shell constructs like absolute or on `$PATH`, shell constructs like
"~" are not expanded), or function that creates "~" are not expanded), or function that creates
an RPC client. Function receives a an RPC client. Function receives a
`dispatchers` table and returns a table with `dispatchers` table and the resolved `config`,
member functions `request`, `notify`, and must return a table with member functions
`is_closing` and `terminate`. See `request`, `notify`, `is_closing` and
|vim.lsp.rpc.request()|, `terminate`. See |vim.lsp.rpc.request()|,
|vim.lsp.rpc.notify()|. For TCP there is a |vim.lsp.rpc.notify()|. For TCP there is a
builtin RPC client factory: builtin RPC client factory:
|vim.lsp.rpc.connect()| |vim.lsp.rpc.connect()|

View File

@ -283,6 +283,8 @@ LSP
its parameters. its parameters.
• |vim.lsp.Config| gained `workspace_required`. • |vim.lsp.Config| gained `workspace_required`.
• `root_markers` in |vim.lsp.Config| can now be ordered by priority. • `root_markers` in |vim.lsp.Config| can now be ordered by priority.
• The function form of `cmd` in a vim.lsp.Config or vim.lsp.ClientConfig
receives the resolved config as the second arg: `cmd(dispatchers, config)`.
LUA LUA

View File

@ -45,11 +45,11 @@ local validate = vim.validate
--- ---
--- Command `string[]` that launches the language server (treated as in |jobstart()|, must be --- Command `string[]` that launches the language server (treated as in |jobstart()|, must be
--- absolute or on `$PATH`, shell constructs like "~" are not expanded), or function that creates an --- absolute or on `$PATH`, shell constructs like "~" are not expanded), or function that creates an
--- RPC client. Function receives a `dispatchers` table and returns a table with member functions --- RPC client. Function receives a `dispatchers` table and the resolved `config`, and must return
--- `request`, `notify`, `is_closing` and `terminate`. --- a table with member functions `request`, `notify`, `is_closing` and `terminate`.
--- See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|. --- See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|.
--- For TCP there is a builtin RPC client factory: |vim.lsp.rpc.connect()| --- For TCP there is a builtin RPC client factory: |vim.lsp.rpc.connect()|
--- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient --- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.PublicClient
--- ---
--- Directory to launch the `cmd` process. Not related to `root_dir`. --- Directory to launch the `cmd` process. Not related to `root_dir`.
--- (default: cwd) --- (default: cwd)
@ -455,7 +455,7 @@ function Client.create(config)
-- Start the RPC client. -- Start the RPC client.
local config_cmd = config.cmd local config_cmd = config.cmd
if type(config_cmd) == 'function' then if type(config_cmd) == 'function' then
self.rpc = config_cmd(dispatchers) self.rpc = config_cmd(dispatchers, config)
else else
self.rpc = lsp.rpc.start(config_cmd, dispatchers, { self.rpc = lsp.rpc.start(config_cmd, dispatchers, {
cwd = config.cmd_cwd, cwd = config.cmd_cwd,

View File

@ -54,7 +54,7 @@ M.create_server_definition = function()
local server = {} local server = {}
server.messages = {} server.messages = {}
function server.cmd(dispatchers) function server.cmd(dispatchers, _config)
local closing = false local closing = false
local handlers = opts.handlers or {} local handlers = opts.handlers or {}
local srv = {} local srv = {}

View File

@ -6490,7 +6490,7 @@ describe('LSP', function()
) )
end) end)
it('supports async function for root_dir', function() it('async root_dir, cmd(…,config) gets resolved config', function()
exec_lua(create_server_definition) exec_lua(create_server_definition)
local tmp1 = t.tmpname(true) local tmp1 = t.tmpname(true)
@ -6504,7 +6504,10 @@ describe('LSP', function()
}) })
vim.lsp.config('foo', { vim.lsp.config('foo', {
cmd = server.cmd, cmd = function(dispatchers, config)
_G.test_resolved_root = config.root_dir --[[@type string]]
return server.cmd(dispatchers, config)
end,
filetypes = { 'foo' }, filetypes = { 'foo' },
root_dir = function(bufnr, cb) root_dir = function(bufnr, cb)
assert(tmp1 == vim.api.nvim_buf_get_name(bufnr)) assert(tmp1 == vim.api.nvim_buf_get_name(bufnr))
@ -6527,6 +6530,12 @@ describe('LSP', function()
end) end)
) )
end) end)
eq(
'some_dir',
exec_lua(function()
return _G.test_resolved_root
end)
)
end) end)
it('starts correct LSP and stops incorrect LSP when filetype changes', function() it('starts correct LSP and stops incorrect LSP when filetype changes', function()
@ -6784,10 +6793,8 @@ describe('LSP', function()
markers_resolve_to({ 'marker_a', { 'marker_b', 'marker_d' } }, tmp_root) markers_resolve_to({ 'marker_a', { 'marker_b', 'marker_d' } }, tmp_root)
markers_resolve_to({ 'foo', { 'bar', 'baz' }, 'marker_d' }, dir_b) markers_resolve_to({ 'foo', { 'bar', 'baz' }, 'marker_d' }, dir_b)
end) end)
end)
describe('vim.lsp.is_enabled()', function() it('vim.lsp.is_enabled()', function()
it('works', function()
exec_lua(function() exec_lua(function()
vim.lsp.config('foo', { vim.lsp.config('foo', {
cmd = { 'foo' }, cmd = { 'foo' },