mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	feat(fs): add vim.fs.root (#28477)
vim.fs.root() is a function for finding a project root relative to a buffer using one or more "root markers". This is useful for LSP and could be useful for other "projects" designs, as well as for any plugins which work with a "projects" concept.
This commit is contained in:
		| @@ -34,7 +34,7 @@ Follow these steps to get LSP features: | |||||||
|     vim.lsp.start({ |     vim.lsp.start({ | ||||||
|       name = 'my-server-name', |       name = 'my-server-name', | ||||||
|       cmd = {'name-of-language-server-executable'}, |       cmd = {'name-of-language-server-executable'}, | ||||||
|       root_dir = vim.fs.dirname(vim.fs.find({'setup.py', 'pyproject.toml'}, { upward = true })[1]), |       root_dir = vim.fs.root(0, {'setup.py', 'pyproject.toml'}), | ||||||
|     }) |     }) | ||||||
| < | < | ||||||
| 3. Check that the server attached to the buffer: > | 3. Check that the server attached to the buffer: > | ||||||
| @@ -836,7 +836,7 @@ start({config}, {opts})                                      *vim.lsp.start()* | |||||||
|         vim.lsp.start({ |         vim.lsp.start({ | ||||||
|            name = 'my-server-name', |            name = 'my-server-name', | ||||||
|            cmd = {'name-of-language-server-executable'}, |            cmd = {'name-of-language-server-executable'}, | ||||||
|            root_dir = vim.fs.dirname(vim.fs.find({'pyproject.toml', 'setup.py'}, { upward = true })[1]), |            root_dir = vim.fs.root(0, {'pyproject.toml', 'setup.py'}), | ||||||
|         }) |         }) | ||||||
| < | < | ||||||
|  |  | ||||||
| @@ -848,7 +848,7 @@ start({config}, {opts})                                      *vim.lsp.start()* | |||||||
|       |vim.lsp.start_client()|. |       |vim.lsp.start_client()|. | ||||||
|     • `root_dir` path to the project root. By default this is used to decide |     • `root_dir` path to the project root. By default this is used to decide | ||||||
|       if an existing client should be re-used. The example above uses |       if an existing client should be re-used. The example above uses | ||||||
|       |vim.fs.find()| and |vim.fs.dirname()| to detect the root by traversing |       |vim.fs.root()| and |vim.fs.dirname()| to detect the root by traversing | ||||||
|       the file system upwards starting from the current directory until either |       the file system upwards starting from the current directory until either | ||||||
|       a `pyproject.toml` or `setup.py` file is found. |       a `pyproject.toml` or `setup.py` file is found. | ||||||
|     • `workspace_folders` list of `{ uri:string, name: string }` tables |     • `workspace_folders` list of `{ uri:string, name: string }` tables | ||||||
|   | |||||||
| @@ -2891,13 +2891,6 @@ vim.fs.find({names}, {opts})                                   *vim.fs.find()* | |||||||
|     narrow the search to find only that type. |     narrow the search to find only that type. | ||||||
|  |  | ||||||
|     Examples: >lua |     Examples: >lua | ||||||
|         -- location of Cargo.toml from the current buffer's path |  | ||||||
|         local cargo = vim.fs.find('Cargo.toml', { |  | ||||||
|           upward = true, |  | ||||||
|           stop = vim.uv.os_homedir(), |  | ||||||
|           path = vim.fs.dirname(vim.api.nvim_buf_get_name(0)), |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
|         -- list all test directories under the runtime directory |         -- list all test directories under the runtime directory | ||||||
|         local test_dirs = vim.fs.find( |         local test_dirs = vim.fs.find( | ||||||
|           {'test', 'tst', 'testdir'}, |           {'test', 'tst', 'testdir'}, | ||||||
| @@ -3013,6 +3006,35 @@ vim.fs.parents({start})                                     *vim.fs.parents()* | |||||||
|         (`nil`) |         (`nil`) | ||||||
|         (`string?`) |         (`string?`) | ||||||
|  |  | ||||||
|  | vim.fs.root({source}, {marker})                                *vim.fs.root()* | ||||||
|  |     Find the first parent directory containing a specific "marker", relative | ||||||
|  |     to a buffer's directory. | ||||||
|  |  | ||||||
|  |     Example: >lua | ||||||
|  |         -- Find the root of a Python project, starting from file 'main.py' | ||||||
|  |         vim.fs.root(vim.fs.joinpath(vim.env.PWD, 'main.py'), {'pyproject.toml', 'setup.py' }) | ||||||
|  |  | ||||||
|  |         -- Find the root of a git repository | ||||||
|  |         vim.fs.root(0, '.git') | ||||||
|  |  | ||||||
|  |         -- Find the parent directory containing any file with a .csproj extension | ||||||
|  |         vim.fs.root(0, function(name, path) | ||||||
|  |           return name:match('%.csproj$') ~= nil | ||||||
|  |         end) | ||||||
|  | < | ||||||
|  |  | ||||||
|  |     Parameters: ~ | ||||||
|  |       • {source}  (`integer|string`) Buffer number (0 for current buffer) or | ||||||
|  |                   file path to begin the search from. | ||||||
|  |       • {marker}  (`string|string[]|fun(name: string, path: string): boolean`) | ||||||
|  |                   A marker, or list of markers, to search for. If a function, | ||||||
|  |                   the function is called for each evaluated item and should | ||||||
|  |                   return true if {name} and {path} are a match. | ||||||
|  |  | ||||||
|  |     Return: ~ | ||||||
|  |         (`string?`) Directory path containing one of the given markers, or nil | ||||||
|  |         if no directory was found. | ||||||
|  |  | ||||||
|  |  | ||||||
| ============================================================================== | ============================================================================== | ||||||
| Lua module: vim.glob                                                *vim.glob* | Lua module: vim.glob                                                *vim.glob* | ||||||
|   | |||||||
| @@ -365,6 +365,9 @@ The following new APIs and features were added. | |||||||
|  |  | ||||||
| • Added built-in |commenting| support. | • Added built-in |commenting| support. | ||||||
|  |  | ||||||
|  | • |vim.fs.root()| finds project root directories from a list of "root | ||||||
|  |   markers". | ||||||
|  |  | ||||||
| ============================================================================== | ============================================================================== | ||||||
| CHANGED FEATURES                                                 *news-changed* | CHANGED FEATURES                                                 *news-changed* | ||||||
|  |  | ||||||
|   | |||||||
| @@ -197,13 +197,6 @@ end | |||||||
| --- Examples: | --- Examples: | ||||||
| --- | --- | ||||||
| --- ```lua | --- ```lua | ||||||
| --- -- location of Cargo.toml from the current buffer's path |  | ||||||
| --- local cargo = vim.fs.find('Cargo.toml', { |  | ||||||
| ---   upward = true, |  | ||||||
| ---   stop = vim.uv.os_homedir(), |  | ||||||
| ---   path = vim.fs.dirname(vim.api.nvim_buf_get_name(0)), |  | ||||||
| --- }) |  | ||||||
| --- |  | ||||||
| --- -- list all test directories under the runtime directory | --- -- list all test directories under the runtime directory | ||||||
| --- local test_dirs = vim.fs.find( | --- local test_dirs = vim.fs.find( | ||||||
| ---   {'test', 'tst', 'testdir'}, | ---   {'test', 'tst', 'testdir'}, | ||||||
| @@ -334,6 +327,56 @@ function M.find(names, opts) | |||||||
|   return matches |   return matches | ||||||
| end | end | ||||||
|  |  | ||||||
|  | --- Find the first parent directory containing a specific "marker", relative to a buffer's | ||||||
|  | --- directory. | ||||||
|  | --- | ||||||
|  | --- Example: | ||||||
|  | --- | ||||||
|  | --- ```lua | ||||||
|  | --- -- Find the root of a Python project, starting from file 'main.py' | ||||||
|  | --- vim.fs.root(vim.fs.joinpath(vim.env.PWD, 'main.py'), {'pyproject.toml', 'setup.py' }) | ||||||
|  | --- | ||||||
|  | --- -- Find the root of a git repository | ||||||
|  | --- vim.fs.root(0, '.git') | ||||||
|  | --- | ||||||
|  | --- -- Find the parent directory containing any file with a .csproj extension | ||||||
|  | --- vim.fs.root(0, function(name, path) | ||||||
|  | ---   return name:match('%.csproj$') ~= nil | ||||||
|  | --- end) | ||||||
|  | --- ``` | ||||||
|  | --- | ||||||
|  | --- @param source integer|string Buffer number (0 for current buffer) or file path to begin the | ||||||
|  | ---               search from. | ||||||
|  | --- @param marker (string|string[]|fun(name: string, path: string): boolean) A marker, or list | ||||||
|  | ---               of markers, to search for. If a function, the function is called for each | ||||||
|  | ---               evaluated item and should return true if {name} and {path} are a match. | ||||||
|  | --- @return string? # Directory path containing one of the given markers, or nil if no directory was | ||||||
|  | ---         found. | ||||||
|  | function M.root(source, marker) | ||||||
|  |   assert(source, 'missing required argument: source') | ||||||
|  |   assert(marker, 'missing required argument: marker') | ||||||
|  |  | ||||||
|  |   local path ---@type string | ||||||
|  |   if type(source) == 'string' then | ||||||
|  |     path = source | ||||||
|  |   elseif type(source) == 'number' then | ||||||
|  |     path = vim.api.nvim_buf_get_name(source) | ||||||
|  |   else | ||||||
|  |     error('invalid type for argument "source": expected string or buffer number') | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   local paths = M.find(marker, { | ||||||
|  |     upward = true, | ||||||
|  |     path = path, | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   if #paths == 0 then | ||||||
|  |     return nil | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   return vim.fs.dirname(paths[1]) | ||||||
|  | end | ||||||
|  |  | ||||||
| --- Split a Windows path into a prefix and a body, such that the body can be processed like a POSIX | --- Split a Windows path into a prefix and a body, such that the body can be processed like a POSIX | ||||||
| --- path. The path must use forward slashes as path separator. | --- path. The path must use forward slashes as path separator. | ||||||
| --- | --- | ||||||
|   | |||||||
| @@ -210,7 +210,7 @@ end | |||||||
| --- vim.lsp.start({ | --- vim.lsp.start({ | ||||||
| ---    name = 'my-server-name', | ---    name = 'my-server-name', | ||||||
| ---    cmd = {'name-of-language-server-executable'}, | ---    cmd = {'name-of-language-server-executable'}, | ||||||
| ---    root_dir = vim.fs.dirname(vim.fs.find({'pyproject.toml', 'setup.py'}, { upward = true })[1]), | ---    root_dir = vim.fs.root(0, {'pyproject.toml', 'setup.py'}), | ||||||
| --- }) | --- }) | ||||||
| --- ``` | --- ``` | ||||||
| --- | --- | ||||||
| @@ -219,9 +219,9 @@ end | |||||||
| --- - `name` arbitrary name for the LSP client. Should be unique per language server. | --- - `name` arbitrary name for the LSP client. Should be unique per language server. | ||||||
| --- - `cmd` command string[] or function, described at |vim.lsp.start_client()|. | --- - `cmd` command string[] or function, described at |vim.lsp.start_client()|. | ||||||
| --- - `root_dir` path to the project root. By default this is used to decide if an existing client | --- - `root_dir` path to the project root. By default this is used to decide if an existing client | ||||||
| ---   should be re-used. The example above uses |vim.fs.find()| and |vim.fs.dirname()| to detect the | ---   should be re-used. The example above uses |vim.fs.root()| and |vim.fs.dirname()| to detect | ||||||
| ---   root by traversing the file system upwards starting from the current directory until either | ---   the root by traversing the file system upwards starting from the current directory until | ||||||
| ---   a `pyproject.toml` or `setup.py` file is found. | ---   either a `pyproject.toml` or `setup.py` file is found. | ||||||
| --- - `workspace_folders` list of `{ uri:string, name: string }` tables specifying the project root | --- - `workspace_folders` list of `{ uri:string, name: string }` tables specifying the project root | ||||||
| ---   folders used by the language server. If `nil` the property is derived from `root_dir` for | ---   folders used by the language server. If `nil` the property is derived from `root_dir` for | ||||||
| ---   convenience. | ---   convenience. | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ local eq = t.eq | |||||||
| local mkdir_p = n.mkdir_p | local mkdir_p = n.mkdir_p | ||||||
| local rmdir = n.rmdir | local rmdir = n.rmdir | ||||||
| local nvim_dir = n.nvim_dir | local nvim_dir = n.nvim_dir | ||||||
|  | local command = n.command | ||||||
|  | local api = n.api | ||||||
| local test_build_dir = t.paths.test_build_dir | local test_build_dir = t.paths.test_build_dir | ||||||
| local test_source_path = t.paths.test_source_path | local test_source_path = t.paths.test_source_path | ||||||
| local nvim_prog = n.nvim_prog | local nvim_prog = n.nvim_prog | ||||||
| @@ -278,6 +280,38 @@ describe('vim.fs', function() | |||||||
|     end) |     end) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|  |   describe('root()', function() | ||||||
|  |     before_each(function() | ||||||
|  |       command('edit test/functional/fixtures/tty-test.c') | ||||||
|  |     end) | ||||||
|  |  | ||||||
|  |     it('works with a single marker', function() | ||||||
|  |       eq(test_source_path, exec_lua([[return vim.fs.root(0, '.git')]])) | ||||||
|  |     end) | ||||||
|  |  | ||||||
|  |     it('works with multiple markers', function() | ||||||
|  |       local bufnr = api.nvim_get_current_buf() | ||||||
|  |       eq( | ||||||
|  |         vim.fs.joinpath(test_source_path, 'test/functional/fixtures'), | ||||||
|  |         exec_lua([[return vim.fs.root(..., {'CMakeLists.txt', '.git'})]], bufnr) | ||||||
|  |       ) | ||||||
|  |     end) | ||||||
|  |  | ||||||
|  |     it('works with a function', function() | ||||||
|  |       ---@type string | ||||||
|  |       local result = exec_lua([[ | ||||||
|  |         return vim.fs.root(0, function(name, path) | ||||||
|  |           return name:match('%.txt$') | ||||||
|  |         end) | ||||||
|  |       ]]) | ||||||
|  |       eq(vim.fs.joinpath(test_source_path, 'test/functional/fixtures'), result) | ||||||
|  |     end) | ||||||
|  |  | ||||||
|  |     it('works with a filename argument', function() | ||||||
|  |       eq(test_source_path, exec_lua([[return vim.fs.root(..., '.git')]], nvim_prog)) | ||||||
|  |     end) | ||||||
|  |   end) | ||||||
|  |  | ||||||
|   describe('joinpath()', function() |   describe('joinpath()', function() | ||||||
|     it('works', function() |     it('works', function() | ||||||
|       eq('foo/bar/baz', vim.fs.joinpath('foo', 'bar', 'baz')) |       eq('foo/bar/baz', vim.fs.joinpath('foo', 'bar', 'baz')) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Gregory Anders
					Gregory Anders