2022-05-15 13:38:19 -07:00
|
|
|
local M = {}
|
|
|
|
|
2023-06-03 03:06:00 -07:00
|
|
|
local iswin = vim.uv.os_uname().sysname == 'Windows_NT'
|
2024-03-29 09:23:01 -07:00
|
|
|
local os_sep = iswin and '\\' or '/'
|
2022-12-01 08:15:05 -07:00
|
|
|
|
2023-07-19 09:55:35 -07:00
|
|
|
--- Iterate over all the parents of the given path.
|
2022-05-15 13:38:19 -07:00
|
|
|
---
|
|
|
|
--- Example:
|
2023-09-14 06:23:01 -07:00
|
|
|
---
|
|
|
|
--- ```lua
|
2022-05-15 13:38:19 -07:00
|
|
|
--- 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
|
|
|
|
--- root_dir = dir
|
|
|
|
--- break
|
|
|
|
--- end
|
|
|
|
--- end
|
|
|
|
---
|
|
|
|
--- if root_dir then
|
|
|
|
--- print("Found git repository at", root_dir)
|
|
|
|
--- end
|
2023-09-14 06:23:01 -07:00
|
|
|
--- ```
|
2022-05-15 13:38:19 -07:00
|
|
|
---
|
2023-07-19 09:55:35 -07:00
|
|
|
---@param start (string) Initial path.
|
2023-08-08 03:58:29 -07:00
|
|
|
---@return fun(_, dir: string): string? # Iterator
|
|
|
|
---@return nil
|
|
|
|
---@return string|nil
|
2022-05-15 13:38:19 -07:00
|
|
|
function M.parents(start)
|
|
|
|
return function(_, dir)
|
2022-05-15 18:53:23 -07:00
|
|
|
local parent = M.dirname(dir)
|
2022-05-15 13:38:19 -07:00
|
|
|
if parent == dir then
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
|
|
|
return parent
|
|
|
|
end,
|
|
|
|
nil,
|
|
|
|
start
|
|
|
|
end
|
|
|
|
|
2023-07-19 09:55:35 -07:00
|
|
|
--- Return the parent directory of the given path
|
2022-05-15 18:53:23 -07:00
|
|
|
---
|
2024-03-06 09:18:00 -07:00
|
|
|
---@generic T : string|nil
|
|
|
|
---@param file T Path
|
|
|
|
---@return T Parent directory of {file}
|
2022-05-15 18:53:23 -07:00
|
|
|
function M.dirname(file)
|
2022-06-03 05:59:19 -07:00
|
|
|
if file == nil then
|
|
|
|
return nil
|
|
|
|
end
|
2022-12-01 08:15:05 -07:00
|
|
|
vim.validate({ file = { file, 's' } })
|
2024-03-29 09:23:01 -07:00
|
|
|
if iswin then
|
|
|
|
file = file:gsub(os_sep, '/') --[[@as string]]
|
|
|
|
if file:match('^%w:/?$') then
|
|
|
|
return file
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if not file:match('/') then
|
2022-12-01 08:15:05 -07:00
|
|
|
return '.'
|
|
|
|
elseif file == '/' or file:match('^/[^/]+$') then
|
|
|
|
return '/'
|
|
|
|
end
|
2024-03-06 09:18:00 -07:00
|
|
|
---@type string
|
2024-03-29 09:23:01 -07:00
|
|
|
local dir = file:match('/$') and file:sub(1, #file - 1) or file:match('^(/?.+)/')
|
2022-12-01 08:15:05 -07:00
|
|
|
if iswin and dir:match('^%w:$') then
|
|
|
|
return dir .. '/'
|
|
|
|
end
|
2024-03-29 09:23:01 -07:00
|
|
|
return dir
|
2022-05-15 18:53:23 -07:00
|
|
|
end
|
|
|
|
|
2023-07-19 09:55:35 -07:00
|
|
|
--- Return the basename of the given path
|
2022-05-15 18:55:18 -07:00
|
|
|
---
|
2024-03-06 09:18:00 -07:00
|
|
|
---@generic T : string|nil
|
|
|
|
---@param file T Path
|
|
|
|
---@return T Basename of {file}
|
2022-05-15 18:55:18 -07:00
|
|
|
function M.basename(file)
|
2022-12-01 08:15:05 -07:00
|
|
|
if file == nil then
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
vim.validate({ file = { file, 's' } })
|
2024-03-29 09:23:01 -07:00
|
|
|
if iswin then
|
|
|
|
file = file:gsub(os_sep, '/') --[[@as string]]
|
|
|
|
if file:match('^%w:/?$') then
|
|
|
|
return ''
|
|
|
|
end
|
2022-12-01 08:15:05 -07:00
|
|
|
end
|
2024-03-29 09:23:01 -07:00
|
|
|
return file:match('/$') and '' or (file:match('[^/]*$'))
|
2022-05-15 18:55:18 -07:00
|
|
|
end
|
|
|
|
|
2023-06-09 18:37:05 -07:00
|
|
|
--- Concatenate directories and/or file paths into a single path with normalization
|
2023-05-20 08:30:48 -07:00
|
|
|
--- (e.g., `"foo/"` and `"bar"` get joined to `"foo/bar"`)
|
|
|
|
---
|
2023-05-17 03:42:18 -07:00
|
|
|
---@param ... string
|
|
|
|
---@return string
|
2023-05-20 08:30:48 -07:00
|
|
|
function M.joinpath(...)
|
2023-01-03 10:24:14 -07:00
|
|
|
return (table.concat({ ... }, '/'):gsub('//+', '/'))
|
2022-12-13 06:59:31 -07:00
|
|
|
end
|
|
|
|
|
2023-03-26 04:46:24 -07:00
|
|
|
---@alias Iterator fun(): string?, string?
|
|
|
|
|
2023-07-19 09:55:35 -07:00
|
|
|
--- Return an iterator over the items located in {path}
|
2022-05-15 19:10:12 -07:00
|
|
|
---
|
|
|
|
---@param path (string) An absolute or relative path to the directory to iterate
|
2022-05-17 07:49:33 -07:00
|
|
|
--- over. The path is first normalized |vim.fs.normalize()|.
|
2022-12-13 06:59:31 -07:00
|
|
|
--- @param opts table|nil Optional keyword arguments:
|
|
|
|
--- - depth: integer|nil How deep the traverse (default 1)
|
|
|
|
--- - skip: (fun(dir_name: string): boolean)|nil Predicate
|
|
|
|
--- to control traversal. Return false to stop searching the current directory.
|
|
|
|
--- Only useful when depth > 1
|
|
|
|
---
|
2023-07-19 09:55:35 -07:00
|
|
|
---@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".
|
2022-12-13 06:59:31 -07:00
|
|
|
function M.dir(path, opts)
|
|
|
|
opts = opts or {}
|
|
|
|
|
|
|
|
vim.validate({
|
|
|
|
path = { path, { 'string' } },
|
|
|
|
depth = { opts.depth, { 'number' }, true },
|
|
|
|
skip = { opts.skip, { 'function' }, true },
|
|
|
|
})
|
|
|
|
|
|
|
|
if not opts.depth or opts.depth == 1 then
|
2023-06-03 03:06:00 -07:00
|
|
|
local fs = vim.uv.fs_scandir(M.normalize(path))
|
2023-03-26 04:46:24 -07:00
|
|
|
return function()
|
|
|
|
if not fs then
|
|
|
|
return
|
|
|
|
end
|
2023-06-03 03:06:00 -07:00
|
|
|
return vim.uv.fs_scandir_next(fs)
|
2023-03-26 04:46:24 -07:00
|
|
|
end
|
2022-12-13 06:59:31 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
--- @async
|
|
|
|
return coroutine.wrap(function()
|
|
|
|
local dirs = { { path, 1 } }
|
|
|
|
while #dirs > 0 do
|
2023-08-08 03:58:29 -07:00
|
|
|
--- @type string, integer
|
2022-12-13 06:59:31 -07:00
|
|
|
local dir0, level = unpack(table.remove(dirs, 1))
|
2023-05-20 08:30:48 -07:00
|
|
|
local dir = level == 1 and dir0 or M.joinpath(path, dir0)
|
2023-06-03 03:06:00 -07:00
|
|
|
local fs = vim.uv.fs_scandir(M.normalize(dir))
|
2022-12-13 06:59:31 -07:00
|
|
|
while fs do
|
2023-06-03 03:06:00 -07:00
|
|
|
local name, t = vim.uv.fs_scandir_next(fs)
|
2022-12-13 06:59:31 -07:00
|
|
|
if not name then
|
|
|
|
break
|
|
|
|
end
|
2023-05-20 08:30:48 -07:00
|
|
|
local f = level == 1 and name or M.joinpath(dir0, name)
|
2022-12-13 06:59:31 -07:00
|
|
|
coroutine.yield(f, t)
|
|
|
|
if
|
|
|
|
opts.depth
|
|
|
|
and level < opts.depth
|
|
|
|
and t == 'directory'
|
|
|
|
and (not opts.skip or opts.skip(f) ~= false)
|
|
|
|
then
|
|
|
|
dirs[#dirs + 1] = { f, level + 1 }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end)
|
2022-05-15 19:10:12 -07:00
|
|
|
end
|
|
|
|
|
2024-02-27 08:20:32 -07:00
|
|
|
--- @class vim.fs.find.Opts
|
|
|
|
--- @inlinedoc
|
|
|
|
---
|
|
|
|
--- Path to begin searching from. If
|
|
|
|
--- omitted, the |current-directory| is used.
|
2024-03-06 03:03:55 -07:00
|
|
|
--- @field path? string
|
2024-02-27 08:20:32 -07:00
|
|
|
---
|
|
|
|
--- Search upward through parent directories.
|
|
|
|
--- Otherwise, search through child directories (recursively).
|
|
|
|
--- (default: `false`)
|
2024-03-06 03:03:55 -07:00
|
|
|
--- @field upward? boolean
|
2024-02-27 08:20:32 -07:00
|
|
|
---
|
|
|
|
--- Stop searching when this directory is reached.
|
|
|
|
--- The directory itself is not searched.
|
2024-03-06 03:03:55 -07:00
|
|
|
--- @field stop? string
|
2024-02-27 08:20:32 -07:00
|
|
|
---
|
|
|
|
--- Find only items of the given type.
|
|
|
|
--- If omitted, all items that match {names} are included.
|
2024-03-06 03:03:55 -07:00
|
|
|
--- @field type? string
|
2024-02-27 08:20:32 -07:00
|
|
|
---
|
|
|
|
--- Stop the search after finding this many matches.
|
|
|
|
--- Use `math.huge` to place no limit on the number of matches.
|
|
|
|
--- (default: `1`)
|
2024-03-06 03:03:55 -07:00
|
|
|
--- @field limit? number
|
2023-08-08 03:58:29 -07:00
|
|
|
|
2023-07-19 09:55:35 -07:00
|
|
|
--- Find files or directories (or other items as specified by `opts.type`) in the given path.
|
2022-05-15 19:37:35 -07:00
|
|
|
---
|
2023-07-19 09:55:35 -07:00
|
|
|
--- 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.
|
2022-05-15 19:37:35 -07:00
|
|
|
---
|
2023-03-01 09:51:22 -07:00
|
|
|
--- Examples:
|
2023-09-14 06:23:01 -07:00
|
|
|
---
|
|
|
|
--- ```lua
|
2023-03-01 09:51:22 -07:00
|
|
|
--- -- location of Cargo.toml from the current buffer's path
|
|
|
|
--- local cargo = vim.fs.find('Cargo.toml', {
|
|
|
|
--- upward = true,
|
2023-06-03 03:06:00 -07:00
|
|
|
--- stop = vim.uv.os_homedir(),
|
2023-03-01 09:51:22 -07:00
|
|
|
--- 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'})
|
2023-09-14 06:23:01 -07:00
|
|
|
--- ```
|
2023-03-01 09:51:22 -07:00
|
|
|
---
|
2023-08-08 03:58:29 -07:00
|
|
|
---@param names (string|string[]|fun(name: string, path: string): boolean) Names of the items to find.
|
2023-03-01 09:51:22 -07:00
|
|
|
--- Must be base names, paths and globs are not supported when {names} is a string or a table.
|
2023-07-19 09:55:35 -07:00
|
|
|
--- If {names} is a function, it is called for each traversed item with args:
|
2023-03-01 09:51:22 -07:00
|
|
|
--- - name: base name of the current item
|
|
|
|
--- - path: full path of the current item
|
2023-07-19 09:55:35 -07:00
|
|
|
--- The function should return `true` if the given item is considered a match.
|
2022-11-23 16:40:07 -07:00
|
|
|
---
|
2024-02-27 08:20:32 -07:00
|
|
|
---@param opts vim.fs.find.Opts Optional keyword arguments:
|
2023-08-08 03:58:29 -07:00
|
|
|
---@return (string[]) # Normalized paths |vim.fs.normalize()| of all matching items
|
2022-05-15 19:37:35 -07:00
|
|
|
function M.find(names, opts)
|
2024-02-27 08:20:32 -07:00
|
|
|
opts = opts or {}
|
2022-05-15 19:37:35 -07:00
|
|
|
vim.validate({
|
2022-09-13 13:16:20 -07:00
|
|
|
names = { names, { 's', 't', 'f' } },
|
2022-05-15 19:37:35 -07:00
|
|
|
path = { opts.path, 's', true },
|
|
|
|
upward = { opts.upward, 'b', true },
|
|
|
|
stop = { opts.stop, 's', true },
|
|
|
|
type = { opts.type, 's', true },
|
|
|
|
limit = { opts.limit, 'n', true },
|
|
|
|
})
|
|
|
|
|
2023-08-08 03:58:29 -07:00
|
|
|
if type(names) == 'string' then
|
|
|
|
names = { names }
|
|
|
|
end
|
2022-05-15 19:37:35 -07:00
|
|
|
|
2024-03-06 03:03:55 -07:00
|
|
|
local path = opts.path or assert(vim.uv.cwd())
|
2022-05-15 19:37:35 -07:00
|
|
|
local stop = opts.stop
|
|
|
|
local limit = opts.limit or 1
|
|
|
|
|
2023-08-08 03:58:29 -07:00
|
|
|
local matches = {} --- @type string[]
|
2022-05-15 19:37:35 -07:00
|
|
|
|
|
|
|
local function add(match)
|
2023-04-04 14:37:46 -07:00
|
|
|
matches[#matches + 1] = M.normalize(match)
|
2022-05-15 19:37:35 -07:00
|
|
|
if #matches == limit then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if opts.upward then
|
2023-08-08 03:58:29 -07:00
|
|
|
local test --- @type fun(p: string): string[]
|
2022-09-13 13:16:20 -07:00
|
|
|
|
|
|
|
if type(names) == 'function' then
|
|
|
|
test = function(p)
|
|
|
|
local t = {}
|
|
|
|
for name, type in M.dir(p) do
|
2023-03-01 09:51:22 -07:00
|
|
|
if (not opts.type or opts.type == type) and names(name, p) then
|
2023-05-20 08:30:48 -07:00
|
|
|
table.insert(t, M.joinpath(p, name))
|
2022-09-13 13:16:20 -07:00
|
|
|
end
|
2022-05-15 19:37:35 -07:00
|
|
|
end
|
2022-09-13 13:16:20 -07:00
|
|
|
return t
|
2022-05-15 19:37:35 -07:00
|
|
|
end
|
2022-09-13 13:16:20 -07:00
|
|
|
else
|
|
|
|
test = function(p)
|
2023-08-08 03:58:29 -07:00
|
|
|
local t = {} --- @type string[]
|
2022-09-13 13:16:20 -07:00
|
|
|
for _, name in ipairs(names) do
|
2023-05-20 08:30:48 -07:00
|
|
|
local f = M.joinpath(p, name)
|
2023-06-03 03:06:00 -07:00
|
|
|
local stat = vim.uv.fs_stat(f)
|
2022-09-13 13:16:20 -07:00
|
|
|
if stat and (not opts.type or opts.type == stat.type) then
|
|
|
|
t[#t + 1] = f
|
|
|
|
end
|
|
|
|
end
|
2022-05-15 19:37:35 -07:00
|
|
|
|
2022-09-13 13:16:20 -07:00
|
|
|
return t
|
|
|
|
end
|
2022-05-15 19:37:35 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
for _, match in ipairs(test(path)) do
|
|
|
|
if add(match) then
|
|
|
|
return matches
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
for parent in M.parents(path) do
|
|
|
|
if stop and parent == stop then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
|
|
|
for _, match in ipairs(test(parent)) do
|
|
|
|
if add(match) then
|
|
|
|
return matches
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
local dirs = { path }
|
|
|
|
while #dirs > 0 do
|
|
|
|
local dir = table.remove(dirs, 1)
|
|
|
|
if stop and dir == stop then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
2022-09-13 13:16:20 -07:00
|
|
|
for other, type_ in M.dir(dir) do
|
2023-05-20 08:30:48 -07:00
|
|
|
local f = M.joinpath(dir, other)
|
2022-09-13 13:16:20 -07:00
|
|
|
if type(names) == 'function' then
|
2023-03-01 09:51:22 -07:00
|
|
|
if (not opts.type or opts.type == type_) and names(other, dir) then
|
2022-05-15 19:37:35 -07:00
|
|
|
if add(f) then
|
|
|
|
return matches
|
|
|
|
end
|
|
|
|
end
|
2022-09-13 13:16:20 -07:00
|
|
|
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
|
2022-05-15 19:37:35 -07:00
|
|
|
end
|
|
|
|
|
2022-09-13 13:16:20 -07:00
|
|
|
if type_ == 'directory' then
|
2022-05-15 19:37:35 -07:00
|
|
|
dirs[#dirs + 1] = f
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return matches
|
|
|
|
end
|
|
|
|
|
2024-04-16 12:13:44 -07:00
|
|
|
--- 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.
|
|
|
|
---
|
|
|
|
--- Does not check if the path is a valid Windows path. Invalid paths will give invalid results.
|
|
|
|
---
|
|
|
|
--- Examples:
|
|
|
|
--- - `//./C:/foo/bar` -> `//./C:`, `/foo/bar`
|
|
|
|
--- - `//?/UNC/server/share/foo/bar` -> `//?/UNC/server/share`, `/foo/bar`
|
|
|
|
--- - `//./system07/C$/foo/bar` -> `//./system07`, `/C$/foo/bar`
|
|
|
|
--- - `C:/foo/bar` -> `C:`, `/foo/bar`
|
|
|
|
--- - `C:foo/bar` -> `C:`, `foo/bar`
|
|
|
|
---
|
|
|
|
--- @param path string Path to split.
|
|
|
|
--- @return string, string, boolean : prefix, body, whether path is invalid.
|
|
|
|
local function split_windows_path(path)
|
|
|
|
local prefix = ''
|
|
|
|
|
|
|
|
--- Match pattern. If there is a match, move the matched pattern from the path to the prefix.
|
|
|
|
--- Returns the matched pattern.
|
|
|
|
---
|
|
|
|
--- @param pattern string Pattern to match.
|
|
|
|
--- @return string|nil Matched pattern
|
|
|
|
local function match_to_prefix(pattern)
|
|
|
|
local match = path:match(pattern)
|
|
|
|
|
|
|
|
if match then
|
|
|
|
prefix = prefix .. match --[[ @as string ]]
|
|
|
|
path = path:sub(#match + 1)
|
|
|
|
end
|
|
|
|
|
|
|
|
return match
|
|
|
|
end
|
|
|
|
|
|
|
|
local function process_unc_path()
|
|
|
|
return match_to_prefix('[^/]+/+[^/]+/+')
|
|
|
|
end
|
|
|
|
|
|
|
|
if match_to_prefix('^//[?.]/') then
|
|
|
|
-- Device paths
|
|
|
|
local device = match_to_prefix('[^/]+/+')
|
|
|
|
|
|
|
|
-- Return early if device pattern doesn't match, or if device is UNC and it's not a valid path
|
|
|
|
if not device or (device:match('^UNC/+$') and not process_unc_path()) then
|
|
|
|
return prefix, path, false
|
|
|
|
end
|
|
|
|
elseif match_to_prefix('^//') then
|
|
|
|
-- Process UNC path, return early if it's invalid
|
|
|
|
if not process_unc_path() then
|
|
|
|
return prefix, path, false
|
|
|
|
end
|
|
|
|
elseif path:match('^%w:') then
|
|
|
|
-- Drive paths
|
|
|
|
prefix, path = path:sub(1, 2), path:sub(3)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- If there are slashes at the end of the prefix, move them to the start of the body. This is to
|
|
|
|
-- ensure that the body is treated as an absolute path. For paths like C:foo/bar, there are no
|
|
|
|
-- slashes at the end of the prefix, so it will be treated as a relative path, as it should be.
|
|
|
|
local trailing_slash = prefix:match('/+$')
|
|
|
|
|
|
|
|
if trailing_slash then
|
|
|
|
prefix = prefix:sub(1, -1 - #trailing_slash)
|
|
|
|
path = trailing_slash .. path --[[ @as string ]]
|
|
|
|
end
|
|
|
|
|
|
|
|
return prefix, path, true
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Resolve `.` and `..` components in a POSIX-style path. This also removes extraneous slashes.
|
|
|
|
--- `..` is not resolved if the path is relative and resolving it requires the path to be absolute.
|
|
|
|
--- If a relative path resolves to the current directory, an empty string is returned.
|
|
|
|
---
|
|
|
|
--- @see M.normalize()
|
|
|
|
--- @param path string Path to resolve.
|
|
|
|
--- @return string Resolved path.
|
|
|
|
local function path_resolve_dot(path)
|
|
|
|
local is_path_absolute = vim.startswith(path, '/')
|
|
|
|
-- Split the path into components and process them
|
|
|
|
local path_components = vim.split(path, '/')
|
|
|
|
local new_path_components = {}
|
|
|
|
|
|
|
|
for _, component in ipairs(path_components) do
|
|
|
|
if component == '.' or component == '' then -- luacheck: ignore 542
|
|
|
|
-- Skip `.` components and empty components
|
|
|
|
elseif component == '..' then
|
|
|
|
if #new_path_components > 0 and new_path_components[#new_path_components] ~= '..' then
|
|
|
|
-- For `..`, remove the last component if we're still inside the current directory, except
|
|
|
|
-- when the last component is `..` itself
|
|
|
|
table.remove(new_path_components)
|
|
|
|
elseif is_path_absolute then -- luacheck: ignore 542
|
|
|
|
-- Reached the root directory in absolute path, do nothing
|
|
|
|
else
|
|
|
|
-- Reached current directory in relative path, add `..` to the path
|
|
|
|
table.insert(new_path_components, component)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
table.insert(new_path_components, component)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/')
|
|
|
|
end
|
|
|
|
|
2024-02-27 08:20:32 -07:00
|
|
|
--- @class vim.fs.normalize.Opts
|
|
|
|
--- @inlinedoc
|
|
|
|
---
|
|
|
|
--- Expand environment variables.
|
|
|
|
--- (default: `true`)
|
2024-04-16 12:13:44 -07:00
|
|
|
--- @field expand_env? boolean
|
|
|
|
---
|
|
|
|
--- Path is a Windows path.
|
|
|
|
--- (default: `true` in Windows, `false` otherwise)
|
|
|
|
--- @field win? boolean
|
|
|
|
|
|
|
|
--- Normalize a path to a standard format. A tilde (~) character at the beginning of the path is
|
|
|
|
--- expanded to the user's home directory and environment variables are also expanded. "." and ".."
|
|
|
|
--- components are also resolved, except when the path is relative and trying to resolve it would
|
|
|
|
--- result in an absolute path.
|
|
|
|
--- - "." as the only part in a relative path:
|
|
|
|
--- - "." => "."
|
|
|
|
--- - "././" => "."
|
|
|
|
--- - ".." when it leads outside the current directory
|
|
|
|
--- - "foo/../../bar" => "../bar"
|
|
|
|
--- - "../../foo" => "../../foo"
|
|
|
|
--- - ".." in the root directory returns the root directory.
|
|
|
|
--- - "/../../" => "/"
|
2024-03-29 09:23:01 -07:00
|
|
|
---
|
|
|
|
--- On Windows, backslash (\) characters are converted to forward slashes (/).
|
2022-05-17 07:49:33 -07:00
|
|
|
---
|
2022-11-23 16:40:07 -07:00
|
|
|
--- Examples:
|
2023-09-14 06:23:01 -07:00
|
|
|
--- ```lua
|
2024-04-16 12:13:44 -07:00
|
|
|
--- [[C:\Users\jdoe]] => "C:/Users/jdoe"
|
|
|
|
--- "~/src/neovim" => "/home/jdoe/src/neovim"
|
|
|
|
--- "$XDG_CONFIG_HOME/nvim/init.vim" => "/Users/jdoe/.config/nvim/init.vim"
|
|
|
|
--- "~/src/nvim/api/../tui/./tui.c" => "/home/jdoe/src/nvim/tui/tui.c"
|
|
|
|
--- "./foo/bar" => "foo/bar"
|
|
|
|
--- "foo/../../../bar" => "../../bar"
|
|
|
|
--- "/home/jdoe/../../../bar" => "/bar"
|
|
|
|
--- "C:foo/../../baz" => "C:../baz"
|
|
|
|
--- "C:/foo/../../baz" => "C:/baz"
|
|
|
|
--- [[\\?\UNC\server\share\foo\..\..\..\bar]] => "//?/UNC/server/share/bar"
|
2023-09-14 06:23:01 -07:00
|
|
|
--- ```
|
2022-05-17 07:49:33 -07:00
|
|
|
---
|
|
|
|
---@param path (string) Path to normalize
|
2024-02-27 08:20:32 -07:00
|
|
|
---@param opts? vim.fs.normalize.Opts
|
|
|
|
---@return (string) : Normalized path
|
2023-03-26 05:01:48 -07:00
|
|
|
function M.normalize(path, opts)
|
|
|
|
opts = opts or {}
|
|
|
|
|
|
|
|
vim.validate({
|
|
|
|
path = { path, { 'string' } },
|
|
|
|
expand_env = { opts.expand_env, { 'boolean' }, true },
|
2024-04-16 12:13:44 -07:00
|
|
|
win = { opts.win, { 'boolean' }, true },
|
2023-03-26 05:01:48 -07:00
|
|
|
})
|
|
|
|
|
2024-04-16 12:13:44 -07:00
|
|
|
local win = opts.win == nil and iswin or not not opts.win
|
|
|
|
local os_sep_local = win and '\\' or '/'
|
|
|
|
|
|
|
|
-- Empty path is already normalized
|
|
|
|
if path == '' then
|
|
|
|
return ''
|
|
|
|
end
|
|
|
|
|
2024-03-29 10:05:02 -07:00
|
|
|
-- Expand ~ to users home directory
|
|
|
|
if vim.startswith(path, '~') then
|
2023-06-03 03:06:00 -07:00
|
|
|
local home = vim.uv.os_homedir() or '~'
|
2024-04-16 12:13:44 -07:00
|
|
|
if home:sub(-1) == os_sep_local then
|
2023-03-26 05:01:48 -07:00
|
|
|
home = home:sub(1, -2)
|
|
|
|
end
|
|
|
|
path = home .. path:sub(2)
|
|
|
|
end
|
|
|
|
|
2024-03-29 10:05:02 -07:00
|
|
|
-- Expand environment variables if `opts.expand_env` isn't `false`
|
2023-03-26 05:01:48 -07:00
|
|
|
if opts.expand_env == nil or opts.expand_env then
|
2023-06-03 03:06:00 -07:00
|
|
|
path = path:gsub('%$([%w_]+)', vim.uv.os_getenv)
|
2023-03-26 05:01:48 -07:00
|
|
|
end
|
|
|
|
|
2024-03-29 10:05:02 -07:00
|
|
|
-- Convert path separator to `/`
|
2024-04-16 12:13:44 -07:00
|
|
|
path = path:gsub(os_sep_local, '/')
|
2024-03-29 10:05:02 -07:00
|
|
|
|
2024-04-16 12:13:44 -07:00
|
|
|
-- Check for double slashes at the start of the path because they have special meaning
|
|
|
|
local double_slash = vim.startswith(path, '//') and not vim.startswith(path, '///')
|
|
|
|
local prefix = ''
|
2024-03-29 10:05:02 -07:00
|
|
|
|
2024-04-16 12:13:44 -07:00
|
|
|
if win then
|
|
|
|
local is_valid --- @type boolean
|
|
|
|
-- Split Windows paths into prefix and body to make processing easier
|
|
|
|
prefix, path, is_valid = split_windows_path(path)
|
|
|
|
|
|
|
|
-- If path is not valid, return it as-is
|
|
|
|
if not is_valid then
|
|
|
|
return prefix .. path
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Remove extraneous slashes from the prefix
|
|
|
|
prefix = prefix:gsub('/+', '/')
|
2023-07-17 23:36:04 -07:00
|
|
|
end
|
2024-03-29 10:05:02 -07:00
|
|
|
|
2024-04-16 12:13:44 -07:00
|
|
|
-- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix
|
|
|
|
-- and path. Preserve leading double slashes as they indicate UNC paths and DOS device paths in
|
|
|
|
-- Windows and have implementation-defined behavior in POSIX.
|
|
|
|
path = (double_slash and '/' or '') .. prefix .. path_resolve_dot(path)
|
|
|
|
|
|
|
|
-- Change empty path to `.`
|
|
|
|
if path == '' then
|
|
|
|
path = '.'
|
|
|
|
end
|
2024-03-29 10:05:02 -07:00
|
|
|
|
|
|
|
return path
|
2022-05-17 07:49:33 -07:00
|
|
|
end
|
|
|
|
|
2022-05-15 13:38:19 -07:00
|
|
|
return M
|