aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/fs.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2025-02-05 23:09:29 +0000
committerJosh Rahm <joshuarahm@gmail.com>2025-02-05 23:09:29 +0000
commitd5f194ce780c95821a855aca3c19426576d28ae0 (patch)
treed45f461b19f9118ad2bb1f440a7a08973ad18832 /runtime/lua/vim/fs.lua
parentc5d770d311841ea5230426cc4c868e8db27300a8 (diff)
parent44740e561fc93afe3ebecfd3618bda2d2abeafb0 (diff)
downloadrneovim-d5f194ce780c95821a855aca3c19426576d28ae0.tar.gz
rneovim-d5f194ce780c95821a855aca3c19426576d28ae0.tar.bz2
rneovim-d5f194ce780c95821a855aca3c19426576d28ae0.zip
Merge remote-tracking branch 'upstream/master' into mix_20240309HEADrahm
Diffstat (limited to 'runtime/lua/vim/fs.lua')
-rw-r--r--runtime/lua/vim/fs.lua158
1 files changed, 141 insertions, 17 deletions
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index d91eeaf02f..8b4242223a 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -1,3 +1,15 @@
+--- @brief <pre>help
+--- *vim.fs.exists()*
+--- Use |uv.fs_stat()| to check a file's type, and whether it exists.
+---
+--- Example:
+---
+--- >lua
+--- if vim.uv.fs_stat(file) then
+--- vim.print("file exists")
+--- end
+--- <
+
local uv = vim.uv
local M = {}
@@ -93,14 +105,23 @@ function M.basename(file)
return file:match('/$') and '' or (file:match('[^/]*$'))
end
---- Concatenate directories and/or file paths into a single path with normalization
---- (e.g., `"foo/"` and `"bar"` get joined to `"foo/bar"`)
+--- Concatenates partial paths (one absolute or relative path followed by zero or more relative
+--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are
+--- replaced with forward-slashes.
+---
+--- Examples:
+--- - "foo/", "/bar" => "foo/bar"
+--- - Windows: "a\foo\", "\bar" => "a/foo/bar"
---
---@since 12
---@param ... string
---@return string
function M.joinpath(...)
- return (table.concat({ ... }, '/'):gsub('//+', '/'))
+ local path = table.concat({ ... }, '/')
+ if iswin then
+ path = path:gsub('\\', '/')
+ end
+ return (path:gsub('//+', '/'))
end
---@alias Iterator fun(): string?, string?
@@ -115,6 +136,7 @@ end
--- - skip: (fun(dir_name: string): boolean)|nil Predicate
--- to control traversal. Return false to stop searching the current directory.
--- Only useful when depth > 1
+--- - follow: boolean|nil Follow symbolic links. (default: true)
---
---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type".
--- "name" is the basename of the item relative to {path}.
@@ -126,6 +148,7 @@ function M.dir(path, opts)
vim.validate('path', path, 'string')
vim.validate('depth', opts.depth, 'number', true)
vim.validate('skip', opts.skip, 'function', true)
+ vim.validate('follow', opts.follow, 'boolean', true)
path = M.normalize(path)
if not opts.depth or opts.depth == 1 then
@@ -156,7 +179,9 @@ function M.dir(path, opts)
if
opts.depth
and level < opts.depth
- and t == 'directory'
+ and (t == 'directory' or (t == 'link' and opts.follow ~= false and (vim.uv.fs_stat(
+ M.joinpath(path, f)
+ ) or {}).type == 'directory'))
and (not opts.skip or opts.skip(f) ~= false)
then
dirs[#dirs + 1] = { f, level + 1 }
@@ -190,6 +215,10 @@ end
--- Use `math.huge` to place no limit on the number of matches.
--- (default: `1`)
--- @field limit? number
+---
+--- Follow symbolic links.
+--- (default: `true`)
+--- @field follow? boolean
--- Find files or directories (or other items as specified by `opts.type`) in the given path.
---
@@ -213,7 +242,7 @@ end
---
--- -- 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$')
+--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$')
--- end, {limit = math.huge, type = 'file'})
--- ```
---
@@ -223,6 +252,7 @@ end
--- 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 vim.fs.find.Opts Optional keyword arguments:
@@ -235,6 +265,7 @@ function M.find(names, opts)
vim.validate('stop', opts.stop, 'string', true)
vim.validate('type', opts.type, 'string', true)
vim.validate('limit', opts.limit, 'number', true)
+ vim.validate('follow', opts.follow, 'boolean', true)
if type(names) == 'string' then
names = { names }
@@ -324,7 +355,14 @@ function M.find(names, opts)
end
end
- if type_ == 'directory' then
+ if
+ type_ == 'directory'
+ or (
+ type_ == 'link'
+ and opts.follow ~= false
+ and (vim.uv.fs_stat(f) or {}).type == 'directory'
+ )
+ then
dirs[#dirs + 1] = f
end
end
@@ -493,6 +531,27 @@ local function path_resolve_dot(path)
return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/')
end
+--- Expand tilde (~) character at the beginning of the path to the user's home directory.
+---
+--- @param path string Path to expand.
+--- @param sep string|nil Path separator to use. Uses os_sep by default.
+--- @return string Expanded path.
+local function expand_home(path, sep)
+ sep = sep or os_sep
+
+ if vim.startswith(path, '~') then
+ local home = uv.os_homedir() or '~' --- @type string
+
+ if home:sub(-1) == sep then
+ home = home:sub(1, -2)
+ end
+
+ path = home .. path:sub(2) --- @type string
+ end
+
+ return path
+end
+
--- @class vim.fs.normalize.Opts
--- @inlinedoc
---
@@ -556,18 +615,12 @@ function M.normalize(path, opts)
return ''
end
- -- Expand ~ to users home directory
- if vim.startswith(path, '~') then
- local home = uv.os_homedir() or '~'
- if home:sub(-1) == os_sep_local then
- home = home:sub(1, -2)
- end
- path = home .. path:sub(2)
- end
+ -- Expand ~ to user's home directory
+ path = expand_home(path, os_sep_local)
-- Expand environment variables if `opts.expand_env` isn't `false`
if opts.expand_env == nil or opts.expand_env then
- path = path:gsub('%$([%w_]+)', uv.os_getenv)
+ path = path:gsub('%$([%w_]+)', uv.os_getenv) --- @type string
end
if win then
@@ -593,8 +646,8 @@ function M.normalize(path, opts)
return prefix .. path
end
- -- Remove extraneous slashes from the prefix
- prefix = prefix:gsub('/+', '/')
+ -- Ensure capital drive and remove extraneous slashes from the prefix
+ prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/')
end
if not opts._fast then
@@ -667,4 +720,75 @@ function M.rm(path, opts)
end
end
+--- Convert path to an absolute path. A tilde (~) character at the beginning of the path is expanded
+--- to the user's home directory. Does not check if the path exists, normalize the path, resolve
+--- symlinks or hardlinks (including `.` and `..`), or expand environment variables. If the path is
+--- already absolute, it is returned unchanged. Also converts `\` path separators to `/`.
+---
+--- @param path string Path
+--- @return string Absolute path
+function M.abspath(path)
+ vim.validate('path', path, 'string')
+
+ -- Expand ~ to user's home directory
+ path = expand_home(path)
+
+ -- Convert path separator to `/`
+ path = path:gsub(os_sep, '/')
+
+ local prefix = ''
+
+ if iswin then
+ prefix, path = split_windows_path(path)
+ end
+
+ if prefix == '//' or vim.startswith(path, '/') then
+ -- Path is already absolute, do nothing
+ return prefix .. path
+ end
+
+ -- Windows allows paths like C:foo/bar, these paths are relative to the current working directory
+ -- of the drive specified in the path
+ local cwd = (iswin and prefix:match('^%w:$')) and uv.fs_realpath(prefix) or uv.cwd()
+ assert(cwd ~= nil)
+ -- Convert cwd path separator to `/`
+ cwd = cwd:gsub(os_sep, '/')
+
+ -- Prefix is not needed for expanding relative paths, as `cwd` already contains it.
+ return M.joinpath(cwd, path)
+end
+
+--- Gets `target` path relative to `base`, or `nil` if `base` is not an ancestor.
+---
+--- Example:
+---
+--- ```lua
+--- vim.fs.relpath('/var', '/var/lib') -- 'lib'
+--- vim.fs.relpath('/var', '/usr/bin') -- nil
+--- ```
+---
+--- @param base string
+--- @param target string
+--- @param opts table? Reserved for future use
+--- @return string|nil
+function M.relpath(base, target, opts)
+ vim.validate('base', base, 'string')
+ vim.validate('target', target, 'string')
+ vim.validate('opts', opts, 'table', true)
+
+ base = vim.fs.normalize(vim.fs.abspath(base))
+ target = vim.fs.normalize(vim.fs.abspath(target))
+ if base == target then
+ return '.'
+ end
+
+ local prefix = ''
+ if iswin then
+ prefix, base = split_windows_path(base)
+ end
+ base = prefix .. base .. (base ~= '/' and '/' or '')
+
+ return vim.startswith(target, base) and target:sub(#base + 1) or nil
+end
+
return M