diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 5a9c9d61f2..578abf72f0 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -41,7 +41,8 @@ Follow these steps to get LSP features: -- 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' }, + -- Nested lists indicate equal priority, see |vim.lsp.Config|. + root_markers = { { '.luarc.json', '.luarc.jsonc' }, '.git' }, -- Specific settings to send to the server. The schema for this is -- defined by the server. For example the schema for lua-language-server @@ -722,9 +723,37 @@ Lua module: vim.lsp *lsp-core* the buffer. Thus a `root_dir()` function can dynamically decide per-buffer whether to activate (or skip) LSP. See example at |vim.lsp.enable()|. - • {root_markers}? (`string[]`) Directory markers (e.g. ".git/", - "package.json") used to decide `root_dir`. Unused if - `root_dir` is provided. + • {root_markers}? (`(string|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. + + The list order decides the priority. To indicate + "equal priority", specify names in a nested list + (`{ { 'a', 'b' }, ... }`) Each entry in this list is + a set of one or more markers. For each set, Nvim will + search upwards for each marker contained in the set. + If a marker is found, the directory which contains + that marker is used as the root directory. If no + markers from the set are found, the process is + repeated with the next set in the list. + + Example: >lua + root_markers = { 'stylua.toml', '.git' } +< + + Find the first parent directory containing the file + `stylua.toml`. If not found, find the first parent + directory containing the file or directory `.git`. + + Example: >lua + root_markers = { { 'stylua.toml', '.luarc.json' }, '.git' } +< + + Find the first parent directory containing EITHER + `stylua.toml` or `.luarc.json`. If not found, find + the first parent directory containing the file or + directory `.git`. buf_attach_client({bufnr}, {client_id}) *vim.lsp.buf_attach_client()* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index a2a3749110..a8362ccdb0 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -70,7 +70,7 @@ HIGHLIGHTS LSP -• todo +• `root_markers` in |vim.lsp.Config| can now be ordered by priority. LUA diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 1f251b47b7..158db3f904 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -293,9 +293,37 @@ end --- example at |vim.lsp.enable()|. --- @field root_dir? string|fun(bufnr: integer, on_dir:fun(root_dir?:string)) --- ---- Directory markers (e.g. ".git/", "package.json") used to decide `root_dir`. Unused if `root_dir` ---- is provided. ---- @field 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. +--- +--- The list order decides the priority. To indicate "equal priority", specify names in a nested list (`{ { 'a', 'b' }, ... }`) +--- Each entry in this list is a set of one or more markers. For each set, Nvim +--- will search upwards for each marker contained in the set. If a marker is +--- found, the directory which contains that marker is used as the root +--- directory. If no markers from the set are found, the process is repeated +--- with the next set in the list. +--- +--- Example: +--- +--- ```lua +--- root_markers = { 'stylua.toml', '.git' } +--- ``` +--- +--- Find the first parent directory containing the file `stylua.toml`. If not +--- found, find the first parent directory containing the file or directory +--- `.git`. +--- +--- Example: +--- +--- ```lua +--- root_markers = { { 'stylua.toml', '.luarc.json' }, '.git' } +--- ``` +--- +--- Find the first parent directory containing EITHER `stylua.toml` or +--- `.luarc.json`. If not found, find the first parent directory containing the +--- file or directory `.git`. +--- +--- @field root_markers? (string|string[])[] --- Sets the default configuration for an LSP client (or _all_ clients if the special name "*" is --- used). @@ -613,7 +641,7 @@ end --- Suppress error reporting if the LSP server fails to start (default false). --- @field silent? boolean --- ---- @field package _root_markers? string[] +--- @field package _root_markers? (string|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`. @@ -662,8 +690,16 @@ function lsp.start(config, opts) local bufnr = vim._resolve_bufnr(opts.bufnr) if not config.root_dir and opts._root_markers then + validate('root_markers', opts._root_markers, 'table') config = vim.deepcopy(config) - config.root_dir = vim.fs.root(bufnr, opts._root_markers) + + for _, marker in ipairs(opts._root_markers) do + local root = vim.fs.root(bufnr, marker) + if root ~= nil then + config.root_dir = root + break + end + end end if diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index ef88230105..7b34e858c8 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -6535,5 +6535,85 @@ describe('LSP', function() vim.lsp.config('*', {}) end) end) + + it('correctly handles root_markers', function() + --- Setup directories for testing + -- root/ + -- ├── dir_a/ + -- │ ├── dir_b/ + -- │ │ ├── target + -- │ │ └── marker_d + -- │ ├── marker_b + -- │ └── marker_c + -- └── marker_a + + ---@param filepath string + local function touch(filepath) + local file = io.open(filepath, 'w') + if file then + file:close() + end + end + + local tmp_root = tmpname(false) + local marker_a = tmp_root .. '/marker_a' + local dir_a = tmp_root .. '/dir_a' + local marker_b = dir_a .. '/marker_b' + local marker_c = dir_a .. '/marker_c' + local dir_b = dir_a .. '/dir_b' + local marker_d = dir_b .. '/marker_d' + local target = dir_b .. '/target' + + mkdir(tmp_root) + touch(marker_a) + mkdir(dir_a) + touch(marker_b) + touch(marker_c) + mkdir(dir_b) + touch(marker_d) + touch(target) + + exec_lua(create_server_definition) + exec_lua(function() + _G._custom_server = _G._create_server() + end) + + ---@param root_markers (string|string[])[] + ---@param expected_root_dir string? + local function markers_resolve_to(root_markers, expected_root_dir) + exec_lua(function() + vim.lsp.config['foo'] = {} + vim.lsp.config('foo', { + cmd = _G._custom_server.cmd, + reuse_client = function() + return false + end, + filetypes = { 'foo' }, + root_markers = root_markers, + }) + vim.lsp.enable('foo') + vim.cmd.edit(target) + vim.bo.filetype = 'foo' + end) + retry(nil, 1000, function() + eq( + expected_root_dir, + exec_lua(function() + local clients = vim.lsp.get_clients() + return clients[#clients].root_dir + end) + ) + end) + end + + markers_resolve_to({ 'marker_d' }, dir_b) + markers_resolve_to({ 'marker_b' }, dir_a) + markers_resolve_to({ 'marker_c' }, dir_a) + markers_resolve_to({ 'marker_a' }, tmp_root) + markers_resolve_to({ 'foo' }, nil) + markers_resolve_to({ { 'marker_b', 'marker_a' }, 'marker_d' }, dir_a) + markers_resolve_to({ 'marker_a', { 'marker_b', 'marker_d' } }, tmp_root) + markers_resolve_to({ 'foo', { 'bar', 'baz' }, 'marker_d' }, dir_b) + end) end) end)