aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/fs.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2023-11-29 21:52:58 +0000
committerJosh Rahm <joshuarahm@gmail.com>2023-11-29 21:52:58 +0000
commit931bffbda3668ddc609fc1da8f9eb576b170aa52 (patch)
treed8c1843a95da5ea0bb4acc09f7e37843d9995c86 /runtime/lua/vim/fs.lua
parent142d9041391780ac15b89886a54015fdc5c73995 (diff)
parent4a8bf24ac690004aedf5540fa440e788459e5e34 (diff)
downloadrneovim-931bffbda3668ddc609fc1da8f9eb576b170aa52.tar.gz
rneovim-931bffbda3668ddc609fc1da8f9eb576b170aa52.tar.bz2
rneovim-931bffbda3668ddc609fc1da8f9eb576b170aa52.zip
Merge remote-tracking branch 'upstream/master' into userreguserreg
Diffstat (limited to 'runtime/lua/vim/fs.lua')
-rw-r--r--runtime/lua/vim/fs.lua214
1 files changed, 138 insertions, 76 deletions
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index a0d2c4c339..22612a7255 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -1,11 +1,12 @@
local M = {}
-local iswin = vim.loop.os_uname().sysname == 'Windows_NT'
+local iswin = vim.uv.os_uname().sysname == 'Windows_NT'
---- Iterate over all the parents of the given file or directory.
+--- Iterate over all the parents of the given path.
---
--- Example:
---- <pre>lua
+---
+--- ```lua
--- local root_dir
--- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do
--- if vim.fn.isdirectory(dir .. "/.git") == 1 then
@@ -17,10 +18,12 @@ local iswin = vim.loop.os_uname().sysname == 'Windows_NT'
--- if root_dir then
--- print("Found git repository at", root_dir)
--- end
---- </pre>
+--- ```
---
----@param start (string) Initial file or directory.
----@return (function) Iterator
+---@param start (string) Initial path.
+---@return fun(_, dir: string): string? # Iterator
+---@return nil
+---@return string|nil
function M.parents(start)
return function(_, dir)
local parent = M.dirname(dir)
@@ -34,10 +37,10 @@ function M.parents(start)
start
end
---- Return the parent directory of the given file or directory
+--- Return the parent directory of the given path
---
----@param file (string) File or directory
----@return (string) Parent directory of {file}
+---@param file (string) Path
+---@return string|nil Parent directory of {file}
function M.dirname(file)
if file == nil then
return nil
@@ -57,10 +60,10 @@ function M.dirname(file)
return (dir:gsub('\\', '/'))
end
---- Return the basename of the given file or directory
+--- Return the basename of the given path
---
----@param file (string) File or directory
----@return (string) Basename of {file}
+---@param file string Path
+---@return string|nil Basename of {file}
function M.basename(file)
if file == nil then
return nil
@@ -72,12 +75,18 @@ function M.basename(file)
return file:match('[/\\]$') and '' or (file:match('[^\\/]*$'):gsub('\\', '/'))
end
----@private
-local function join_paths(...)
+--- Concatenate directories and/or file paths into a single path with normalization
+--- (e.g., `"foo/"` and `"bar"` get joined to `"foo/bar"`)
+---
+---@param ... string
+---@return string
+function M.joinpath(...)
return (table.concat({ ... }, '/'):gsub('//+', '/'))
end
---- Return an iterator over the files and directories located in {path}
+---@alias Iterator fun(): string?, string?
+
+--- Return an iterator over the items located in {path}
---
---@param path (string) An absolute or relative path to the directory to iterate
--- over. The path is first normalized |vim.fs.normalize()|.
@@ -87,9 +96,10 @@ end
--- to control traversal. Return false to stop searching the current directory.
--- Only useful when depth > 1
---
----@return Iterator over files and directories in {path}. Each iteration yields
---- two values: name and type. Each "name" is the basename of the file or
---- directory relative to {path}. Type is one of "file" or "directory".
+---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type".
+--- "name" is the basename of the item relative to {path}.
+--- "type" is one of the following:
+--- "file", "directory", "link", "fifo", "socket", "char", "block", "unknown".
function M.dir(path, opts)
opts = opts or {}
@@ -100,25 +110,29 @@ function M.dir(path, opts)
})
if not opts.depth or opts.depth == 1 then
- return function(fs)
- return vim.loop.fs_scandir_next(fs)
- end,
- vim.loop.fs_scandir(M.normalize(path))
+ local fs = vim.uv.fs_scandir(M.normalize(path))
+ return function()
+ if not fs then
+ return
+ end
+ return vim.uv.fs_scandir_next(fs)
+ end
end
--- @async
return coroutine.wrap(function()
local dirs = { { path, 1 } }
while #dirs > 0 do
+ --- @type string, integer
local dir0, level = unpack(table.remove(dirs, 1))
- local dir = level == 1 and dir0 or join_paths(path, dir0)
- local fs = vim.loop.fs_scandir(M.normalize(dir))
+ local dir = level == 1 and dir0 or M.joinpath(path, dir0)
+ local fs = vim.uv.fs_scandir(M.normalize(dir))
while fs do
- local name, t = vim.loop.fs_scandir_next(fs)
+ local name, t = vim.uv.fs_scandir_next(fs)
if not name then
break
end
- local f = level == 1 and name or join_paths(dir0, name)
+ local f = level == 1 and name or M.joinpath(dir0, name)
coroutine.yield(f, t)
if
opts.depth
@@ -133,22 +147,52 @@ function M.dir(path, opts)
end)
end
---- Find files or directories in the given path.
+--- @class vim.fs.find.opts
+--- @field path string
+--- @field upward boolean
+--- @field stop string
+--- @field type string
+--- @field limit number
+
+--- Find files or directories (or other items as specified by `opts.type`) in the given path.
+---
+--- Finds items given in {names} starting from {path}. If {upward} is "true"
+--- then the search traverses upward through parent directories; otherwise,
+--- the search traverses downward. Note that downward searches are recursive
+--- and may search through many directories! If {stop} is non-nil, then the
+--- search stops when the directory given in {stop} is reached. The search
+--- terminates when {limit} (default 1) matches are found. You can set {type}
+--- to "file", "directory", "link", "socket", "char", "block", or "fifo"
+--- to narrow the search to find only that type.
---
---- Finds any files or directories given in {names} starting from {path}. If
---- {upward} is "true" then the search traverses upward through parent
---- directories; otherwise, the search traverses downward. Note that downward
---- searches are recursive and may search through many directories! If {stop}
---- is non-nil, then the search stops when the directory given in {stop} is
---- reached. The search terminates when {limit} (default 1) matches are found.
---- The search can be narrowed to find only files or only directories by
---- specifying {type} to be "file" or "directory", respectively.
+--- 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'},
+--- {limit = math.huge, type = 'directory', path = './runtime/'}
+--- )
+---
+--- -- get all files ending with .cpp or .hpp inside lib/
+--- local cpp_hpp = vim.fs.find(function(name, path)
+--- return name:match('.*%.[ch]pp$') and path:match('[/\\\\]lib$')
+--- end, {limit = math.huge, type = 'file'})
+--- ```
---
----@param names (string|table|fun(name: string): boolean) Names of the files
---- and directories to find.
---- Must be base names, paths and globs are not supported.
---- The function is called per file and directory within the
---- traversed directories to test if they match {names}.
+---@param names (string|string[]|fun(name: string, path: string): boolean) Names of the items to find.
+--- Must be base names, paths and globs are not supported when {names} is a string or a table.
+--- If {names} is a function, it is called for each traversed item with args:
+--- - name: base name of the current item
+--- - path: full path of the current item
+--- The function should return `true` if the given item is considered a match.
---
---@param opts (table) Optional keyword arguments:
--- - path (string): Path to begin searching from. If
@@ -159,16 +203,14 @@ end
--- (recursively).
--- - stop (string): Stop searching when this directory is
--- reached. The directory itself is not searched.
---- - type (string): Find only files ("file") or
---- directories ("directory"). If omitted, both
---- files and directories that match {names} are
---- included.
+--- - type (string): Find only items of the given type.
+--- If omitted, all items that match {names} are included.
--- - limit (number, default 1): Stop the search after
--- finding this many matches. Use `math.huge` to
--- place no limit on the number of matches.
----@return (table) Normalized paths |vim.fs.normalize()| of all matching files or directories
+---@return (string[]) # Normalized paths |vim.fs.normalize()| of all matching items
function M.find(names, opts)
- opts = opts or {}
+ opts = opts or {} --[[@as vim.fs.find.opts]]
vim.validate({
names = { names, { 's', 't', 'f' } },
path = { opts.path, 's', true },
@@ -178,41 +220,42 @@ function M.find(names, opts)
limit = { opts.limit, 'n', true },
})
- names = type(names) == 'string' and { names } or names
+ if type(names) == 'string' then
+ names = { names }
+ end
- local path = opts.path or vim.loop.cwd()
+ local path = opts.path or vim.uv.cwd()
local stop = opts.stop
local limit = opts.limit or 1
- local matches = {}
+ local matches = {} --- @type string[]
- ---@private
local function add(match)
- matches[#matches + 1] = match
+ matches[#matches + 1] = M.normalize(match)
if #matches == limit then
return true
end
end
if opts.upward then
- local test
+ local test --- @type fun(p: string): string[]
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, join_paths(p, name))
+ if (not opts.type or opts.type == type) and names(name, p) then
+ table.insert(t, M.joinpath(p, name))
end
end
return t
end
else
test = function(p)
- local t = {}
+ local t = {} --- @type string[]
for _, name in ipairs(names) do
- local f = join_paths(p, name)
- local stat = vim.loop.fs_stat(f)
+ local f = M.joinpath(p, name)
+ local stat = vim.uv.fs_stat(f)
if stat and (not opts.type or opts.type == stat.type) then
t[#t + 1] = f
end
@@ -248,9 +291,9 @@ function M.find(names, opts)
end
for other, type_ in M.dir(dir) do
- local f = join_paths(dir, other)
+ local f = M.joinpath(dir, other)
if type(names) == 'function' then
- if names(other) and (not opts.type or opts.type == type_) then
+ if (not opts.type or opts.type == type_) and names(other, dir) then
if add(f) then
return matches
end
@@ -281,28 +324,47 @@ end
--- variables are also expanded.
---
--- Examples:
---- <pre>lua
---- vim.fs.normalize('C:\\\\Users\\\\jdoe')
---- --> 'C:/Users/jdoe'
---
---- vim.fs.normalize('~/src/neovim')
---- --> '/home/jdoe/src/neovim'
+--- ```lua
+--- vim.fs.normalize('C:\\\\Users\\\\jdoe')
+--- -- 'C:/Users/jdoe'
---
---- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')
---- --> '/Users/jdoe/.config/nvim/init.vim'
---- </pre>
+--- vim.fs.normalize('~/src/neovim')
+--- -- '/home/jdoe/src/neovim'
+---
+--- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')
+--- -- '/Users/jdoe/.config/nvim/init.vim'
+--- ```
---
---@param path (string) Path to normalize
+---@param opts table|nil Options:
+--- - expand_env: boolean Expand environment variables (default: true)
---@return (string) Normalized path
-function M.normalize(path)
- vim.validate({ path = { path, 's' } })
- return (
- path
- :gsub('^~$', vim.loop.os_homedir())
- :gsub('^~/', vim.loop.os_homedir() .. '/')
- :gsub('%$([%w_]+)', vim.loop.os_getenv)
- :gsub('\\', '/')
- )
+function M.normalize(path, opts)
+ opts = opts or {}
+
+ vim.validate({
+ path = { path, { 'string' } },
+ expand_env = { opts.expand_env, { 'boolean' }, true },
+ })
+
+ if path:sub(1, 1) == '~' then
+ local home = vim.uv.os_homedir() or '~'
+ if home:sub(-1) == '\\' or home:sub(-1) == '/' then
+ home = home:sub(1, -2)
+ end
+ path = home .. path:sub(2)
+ end
+
+ if opts.expand_env == nil or opts.expand_env then
+ path = path:gsub('%$([%w_]+)', vim.uv.os_getenv)
+ end
+
+ path = path:gsub('\\', '/'):gsub('/+', '/')
+ if iswin and path:match('^%w:/$') then
+ return path
+ end
+ return (path:gsub('(.)/$', '%1'))
end
return M