feat(fs): extend fs.find to accept predicate (#20193)

Makes it possible to use `vim.fs.find` to find files where only a
substring is known.
This is useful for `vim.lsp.start` to get the `root_dir` for languages
where the project-file is only known by its extension, not by the full
name.
For example in .NET projects there is usually a `<projectname>.csproj`
file in the project root.

Example:

    vim.fs.find(function(x) return vim.endswith(x, '.csproj') end, { upward = true })
This commit is contained in:
Mathias Fußenegger 2022-09-13 22:16:20 +02:00 committed by GitHub
parent 1970d2ac43
commit a8c9e721d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 63 additions and 20 deletions

View File

@ -2323,8 +2323,10 @@ find({names}, {opts}) *vim.fs.find()*
specifying {type} to be "file" or "directory", respectively. specifying {type} to be "file" or "directory", respectively.
Parameters: ~ Parameters: ~
{names} (string|table) Names of the files and directories to find. {names} (string|table|fun(name: string): boolean) Names of the files
Must be base names, paths and globs are not supported. and directories to find. Must be base names, paths and globs
are not supported. If a function it is called per file and
dir within the traversed directories to test if they match.
{opts} (table) Optional keyword arguments: {opts} (table) Optional keyword arguments:
• path (string): Path to begin searching from. If omitted, • path (string): Path to begin searching from. If omitted,
the current working directory is used. the current working directory is used.

View File

@ -76,8 +76,11 @@ end
--- The search can be narrowed to find only files or or only directories by --- The search can be narrowed to find only files or or only directories by
--- specifying {type} to be "file" or "directory", respectively. --- specifying {type} to be "file" or "directory", respectively.
--- ---
---@param names (string|table) Names of the files and directories to find. Must ---@param names (string|table|fun(name: string): boolean) Names of the files
--- be base names, paths and globs are not supported. --- and directories to find.
--- Must be base names, paths and globs are not supported.
--- If a function it is called per file and dir within the
--- traversed directories to test if they match.
---@param opts (table) Optional keyword arguments: ---@param opts (table) Optional keyword arguments:
--- - path (string): Path to begin searching from. If --- - path (string): Path to begin searching from. If
--- omitted, the current working directory is used. --- omitted, the current working directory is used.
@ -98,7 +101,7 @@ end
function M.find(names, opts) function M.find(names, opts)
opts = opts or {} opts = opts or {}
vim.validate({ vim.validate({
names = { names, { 's', 't' } }, names = { names, { 's', 't', 'f' } },
path = { opts.path, 's', true }, path = { opts.path, 's', true },
upward = { opts.upward, 'b', true }, upward = { opts.upward, 'b', true },
stop = { opts.stop, 's', true }, stop = { opts.stop, 's', true },
@ -123,18 +126,31 @@ function M.find(names, opts)
end end
if opts.upward then if opts.upward then
---@private local test
local function test(p)
local t = {}
for _, name in ipairs(names) do
local f = p .. '/' .. name
local stat = vim.loop.fs_stat(f)
if stat and (not opts.type or opts.type == stat.type) then
t[#t + 1] = f
end
end
return t if type(names) == 'function' then
test = function(p)
local t = {}
for name, type in M.dir(p) do
if names(name) and (not opts.type or opts.type == type) then
table.insert(t, p .. '/' .. name)
end
end
return t
end
else
test = function(p)
local t = {}
for _, name in ipairs(names) do
local f = p .. '/' .. name
local stat = vim.loop.fs_stat(f)
if stat and (not opts.type or opts.type == stat.type) then
t[#t + 1] = f
end
end
return t
end
end end
for _, match in ipairs(test(path)) do for _, match in ipairs(test(path)) do
@ -162,17 +178,25 @@ function M.find(names, opts)
break break
end end
for other, type in M.dir(dir) do for other, type_ in M.dir(dir) do
local f = dir .. '/' .. other local f = dir .. '/' .. other
for _, name in ipairs(names) do if type(names) == 'function' then
if name == other and (not opts.type or opts.type == type) then if names(other) and (not opts.type or opts.type == type_) then
if add(f) then if add(f) then
return matches return matches
end end
end end
else
for _, name in ipairs(names) do
if name == other and (not opts.type or opts.type == type_) then
if add(f) then
return matches
end
end
end
end end
if type == 'directory' then if type_ == 'directory' then
dirs[#dirs + 1] = f dirs[#dirs + 1] = f
end end
end end

View File

@ -78,6 +78,23 @@ describe('vim.fs', function()
return vim.fs.find(nvim, { path = dir, type = 'file' }) return vim.fs.find(nvim, { path = dir, type = 'file' })
]], test_build_dir, nvim_prog_basename)) ]], test_build_dir, nvim_prog_basename))
end) end)
it('accepts predicate as names', function()
eq({test_build_dir}, exec_lua([[
local dir = ...
local opts = { path = dir, upward = true, type = 'directory' }
return vim.fs.find(function(x) return x == 'build' end, opts)
]], nvim_dir))
eq({nvim_prog}, exec_lua([[
local dir, nvim = ...
return vim.fs.find(function(x) return x == nvim end, { path = dir, type = 'file' })
]], test_build_dir, nvim_prog_basename))
eq({}, exec_lua([[
local dir = ...
local opts = { path = dir, upward = true, type = 'directory' }
return vim.fs.find(function(x) return x == 'no-match' end, opts)
]], nvim_dir))
end)
end) end)
describe('normalize()', function() describe('normalize()', function()