diff options
-rw-r--r-- | runtime/doc/lsp.txt | 6 | ||||
-rw-r--r-- | runtime/doc/lua.txt | 36 | ||||
-rw-r--r-- | runtime/doc/news.txt | 3 | ||||
-rw-r--r-- | runtime/lua/vim/fs.lua | 57 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 8 | ||||
-rw-r--r-- | test/functional/lua/fs_spec.lua | 34 |
6 files changed, 123 insertions, 21 deletions
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 9385d92416..d1fd84669a 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -34,7 +34,7 @@ Follow these steps to get LSP features: vim.lsp.start({ name = 'my-server-name', 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: > @@ -836,7 +836,7 @@ start({config}, {opts}) *vim.lsp.start()* vim.lsp.start({ name = 'my-server-name', 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()|. • `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 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 a `pyproject.toml` or `setup.py` file is found. • `workspace_folders` list of `{ uri:string, name: string }` tables diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index eaf61ff3fe..92c49bca40 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2891,13 +2891,6 @@ vim.fs.find({names}, {opts}) *vim.fs.find()* narrow the search to find only that type. 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 local test_dirs = vim.fs.find( {'test', 'tst', 'testdir'}, @@ -3013,6 +3006,35 @@ vim.fs.parents({start}) *vim.fs.parents()* (`nil`) (`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* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 5e2a48701e..c05e060920 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -365,6 +365,9 @@ The following new APIs and features were added. • Added built-in |commenting| support. +• |vim.fs.root()| finds project root directories from a list of "root + markers". + ============================================================================== CHANGED FEATURES *news-changed* diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 65ad58c720..766dc09691 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -197,13 +197,6 @@ end --- 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 --- local test_dirs = vim.fs.find( --- {'test', 'tst', 'testdir'}, @@ -334,6 +327,56 @@ function M.find(names, opts) return matches 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 --- path. The path must use forward slashes as path separator. --- diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index ab22c5901a..8403aa0ee6 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -210,7 +210,7 @@ end --- vim.lsp.start({ --- name = 'my-server-name', --- 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. --- - `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 ---- should be re-used. The example above uses |vim.fs.find()| and |vim.fs.dirname()| to detect the ---- root by traversing the file system upwards starting from the current directory until either ---- a `pyproject.toml` or `setup.py` file is found. +--- should be re-used. The example above uses |vim.fs.root()| and |vim.fs.dirname()| to detect +--- the root by traversing the file system upwards starting from the current directory until +--- either a `pyproject.toml` or `setup.py` file is found. --- - `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 --- convenience. diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua index db6ff60e9a..b651297db3 100644 --- a/test/functional/lua/fs_spec.lua +++ b/test/functional/lua/fs_spec.lua @@ -7,6 +7,8 @@ local eq = t.eq local mkdir_p = n.mkdir_p local rmdir = n.rmdir 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_source_path = t.paths.test_source_path local nvim_prog = n.nvim_prog @@ -278,6 +280,38 @@ describe('vim.fs', function() 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() it('works', function() eq('foo/bar/baz', vim.fs.joinpath('foo', 'bar', 'baz')) |