From 32f30c4874a0fdfbad427b0f4b699638c53f0ccd Mon Sep 17 00:00:00 2001 From: Julian Visser <12615757+justmejulian@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:52:17 +0200 Subject: [PATCH] feat(lsp): pass resolved config to cmd() #34550 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()`. Co-authored-by: Justin M. Keyes --- runtime/doc/lsp.txt | 10 +++++----- runtime/doc/news.txt | 2 ++ runtime/lua/vim/lsp/client.lua | 8 ++++---- test/functional/plugin/lsp/testutil.lua | 2 +- test/functional/plugin/lsp_spec.lua | 17 ++++++++++++----- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 4ae472b11b..211e1253eb 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -1359,16 +1359,16 @@ Lua module: vim.lsp.client *lsp-client* • Note: To send an empty dictionary use |vim.empty_dict()|, else it will be encoded 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 server (treated as in |jobstart()|, must be 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 `request`, `notify`, - `is_closing` and `terminate`. See - |vim.lsp.rpc.request()|, + `dispatchers` table and the resolved `config`, + and must return a table with member functions + `request`, `notify`, `is_closing` and + `terminate`. See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|. For TCP there is a builtin RPC client factory: |vim.lsp.rpc.connect()| diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 6077cd691a..1cb48da6bc 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -180,6 +180,8 @@ LSP `an` selects outwards and `in` selects inwards. • Support for multiline semantic tokens. • Support for the `disabled` field on code actions. +• 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 diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index d1cd29421f..7d8bf6c1fe 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -45,11 +45,11 @@ local validate = vim.validate --- --- 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 ---- RPC client. Function receives a `dispatchers` table and returns a table with member functions ---- `request`, `notify`, `is_closing` and `terminate`. +--- RPC client. Function receives a `dispatchers` table and the resolved `config`, and must return +--- a table with member functions `request`, `notify`, `is_closing` and `terminate`. --- See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|. --- 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`. --- (default: cwd) @@ -455,7 +455,7 @@ function Client.create(config) -- Start the RPC client. local config_cmd = config.cmd if type(config_cmd) == 'function' then - self.rpc = config_cmd(dispatchers) + self.rpc = config_cmd(dispatchers, config) else self.rpc = lsp.rpc.start(config_cmd, dispatchers, { cwd = config.cmd_cwd, diff --git a/test/functional/plugin/lsp/testutil.lua b/test/functional/plugin/lsp/testutil.lua index 95fc22b96b..5edcc00845 100644 --- a/test/functional/plugin/lsp/testutil.lua +++ b/test/functional/plugin/lsp/testutil.lua @@ -54,7 +54,7 @@ M.create_server_definition = function() local server = {} server.messages = {} - function server.cmd(dispatchers) + function server.cmd(dispatchers, _config) local closing = false local handlers = opts.handlers or {} local srv = {} diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 66b51f67f2..07e85a743c 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -6579,7 +6579,7 @@ describe('LSP', function() ) end) - it('supports async function for root_dir', function() + it('async root_dir, cmd(…,config) gets resolved config', function() exec_lua(create_server_definition) local tmp1 = t.tmpname(true) @@ -6593,7 +6593,10 @@ describe('LSP', function() }) 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' }, root_dir = function(bufnr, cb) assert(tmp1 == vim.api.nvim_buf_get_name(bufnr)) @@ -6616,6 +6619,12 @@ describe('LSP', function() end) ) end) + eq( + 'some_dir', + exec_lua(function() + return _G.test_resolved_root + end) + ) end) it('starts correct LSP and stops incorrect LSP when filetype changes', function() @@ -6842,10 +6851,8 @@ describe('LSP', function() markers_resolve_to({ 'marker_a', { 'marker_b', 'marker_d' } }, tmp_root) markers_resolve_to({ 'foo', { 'bar', 'baz' }, 'marker_d' }, dir_b) end) - end) - describe('vim.lsp.is_enabled()', function() - it('works', function() + it('vim.lsp.is_enabled()', function() exec_lua(function() vim.lsp.config('foo', { cmd = { 'foo' },