aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregory Anders <8965202+gpanders@users.noreply.github.com>2024-04-24 21:43:46 -0500
committerGitHub <noreply@github.com>2024-04-24 21:43:46 -0500
commit38b9c322c97b63f53caef7a651211fc9312d055e (patch)
treeaf8804460c04d3bd4b2965d7583252506a066cf1
parent16513b30337523dc64707309ee7fe3dd2247266d (diff)
downloadrneovim-38b9c322c97b63f53caef7a651211fc9312d055e.tar.gz
rneovim-38b9c322c97b63f53caef7a651211fc9312d055e.tar.bz2
rneovim-38b9c322c97b63f53caef7a651211fc9312d055e.zip
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.
-rw-r--r--runtime/doc/lsp.txt6
-rw-r--r--runtime/doc/lua.txt36
-rw-r--r--runtime/doc/news.txt3
-rw-r--r--runtime/lua/vim/fs.lua57
-rw-r--r--runtime/lua/vim/lsp.lua8
-rw-r--r--test/functional/lua/fs_spec.lua34
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'))