mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	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:
		| @@ -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()* | ||||
|   | ||||
| @@ -70,7 +70,7 @@ HIGHLIGHTS | ||||
|  | ||||
| LSP | ||||
|  | ||||
| • todo | ||||
| • `root_markers` in |vim.lsp.Config| can now be ordered by priority. | ||||
|  | ||||
| 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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Lorenzo Bellina
					Lorenzo Bellina