feat(lsp): root_markers can control priority #33485

Problem:
root_markers cannot specify "equal priority filenames.

Solution:
Support nesting:

    {
      ...
      root_markers = { { ".stylua.toml", ".luarc.json" }, { ".git "} }
      ...
    }


Co-authored-by: Maria José Solano <majosolano99@gmail.com>
Co-authored-by: Gregory Anders <github@gpanders.com>
Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
This commit is contained in:
Lorenzo Bellina
2025-04-30 15:43:32 +02:00
committed by GitHub
parent f27fb737ce
commit 6577d72d81
4 changed files with 155 additions and 10 deletions

View File

@ -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()*

View File

@ -70,7 +70,7 @@ HIGHLIGHTS
LSP
todo
`root_markers` in |vim.lsp.Config| can now be ordered by priority.
LUA

View File

@ -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

View File

@ -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)