mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			813 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			813 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| --- @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 = {}
 | |
| 
 | |
| -- Can't use `has('win32')` because the `nvim -ll` test runner doesn't support `vim.fn` yet.
 | |
| local sysname = uv.os_uname().sysname:lower()
 | |
| local iswin = not not (sysname:find('windows') or sysname:find('mingw'))
 | |
| local os_sep = iswin and '\\' or '/'
 | |
| 
 | |
| --- Iterate over all the parents of the given path (not expanded/resolved, the caller must do that).
 | |
| ---
 | |
| --- Example:
 | |
| ---
 | |
| --- ```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
 | |
| ---     root_dir = dir
 | |
| ---     break
 | |
| ---   end
 | |
| --- end
 | |
| ---
 | |
| --- if root_dir then
 | |
| ---   print('Found git repository at', root_dir)
 | |
| --- end
 | |
| --- ```
 | |
| ---
 | |
| ---@since 10
 | |
| ---@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)
 | |
|     if parent == dir then
 | |
|       return nil
 | |
|     end
 | |
| 
 | |
|     return parent
 | |
|   end,
 | |
|     nil,
 | |
|     start
 | |
| end
 | |
| 
 | |
| --- Gets the parent directory of the given path (not expanded/resolved, the caller must do that).
 | |
| ---
 | |
| ---@since 10
 | |
| ---@generic T : string|nil
 | |
| ---@param file T Path
 | |
| ---@return T Parent directory of {file}
 | |
| function M.dirname(file)
 | |
|   if file == nil then
 | |
|     return nil
 | |
|   end
 | |
|   vim.validate('file', file, 'string')
 | |
|   if iswin then
 | |
|     file = file:gsub(os_sep, '/') --[[@as string]]
 | |
|     if file:match('^%w:/?$') then
 | |
|       return file
 | |
|     end
 | |
|   end
 | |
|   if not file:match('/') then
 | |
|     return '.'
 | |
|   elseif file == '/' or file:match('^/[^/]+$') then
 | |
|     return '/'
 | |
|   end
 | |
|   ---@type string
 | |
|   local dir = file:match('/$') and file:sub(1, #file - 1) or file:match('^(/?.+)/')
 | |
|   if iswin and dir:match('^%w:$') then
 | |
|     return dir .. '/'
 | |
|   end
 | |
|   return dir
 | |
| end
 | |
| 
 | |
| --- Return the basename of the given path
 | |
| ---
 | |
| ---@since 10
 | |
| ---@generic T : string|nil
 | |
| ---@param file T Path
 | |
| ---@return T Basename of {file}
 | |
| function M.basename(file)
 | |
|   if file == nil then
 | |
|     return nil
 | |
|   end
 | |
|   vim.validate('file', file, 'string')
 | |
|   if iswin then
 | |
|     file = file:gsub(os_sep, '/') --[[@as string]]
 | |
|     if file:match('^%w:/?$') then
 | |
|       return ''
 | |
|     end
 | |
|   end
 | |
|   return file:match('/$') and '' or (file:match('[^/]*$'))
 | |
| end
 | |
| 
 | |
| --- 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(...)
 | |
|   local path = table.concat({ ... }, '/')
 | |
|   if iswin then
 | |
|     path = path:gsub('\\', '/')
 | |
|   end
 | |
|   return (path:gsub('//+', '/'))
 | |
| end
 | |
| 
 | |
| --- @class vim.fs.dir.Opts
 | |
| --- @inlinedoc
 | |
| ---
 | |
| --- How deep the traverse.
 | |
| --- (default: `1`)
 | |
| --- @field depth? integer
 | |
| ---
 | |
| --- Predicate to control traversal.
 | |
| --- Return false to stop searching the current directory.
 | |
| --- Only useful when depth > 1
 | |
| --- Return an iterator over the items located in {path}
 | |
| --- @field skip? (fun(dir_name: string): boolean)
 | |
| ---
 | |
| --- Follow symbolic links.
 | |
| --- (default: `false`)
 | |
| --- @field follow? boolean
 | |
| 
 | |
| ---@alias Iterator fun(): string?, string?
 | |
| 
 | |
| --- Return an iterator over the items located in {path}
 | |
| ---
 | |
| ---@since 10
 | |
| ---@param path (string) An absolute or relative path to the directory to iterate
 | |
| ---            over. The path is first normalized |vim.fs.normalize()|.
 | |
| ---@param opts? vim.fs.dir.Opts Optional keyword arguments:
 | |
| ---@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 {}
 | |
| 
 | |
|   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
 | |
|     local fs = uv.fs_scandir(path)
 | |
|     return function()
 | |
|       if not fs then
 | |
|         return
 | |
|       end
 | |
|       return 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 M.joinpath(path, dir0)
 | |
|       local fs = uv.fs_scandir(dir)
 | |
|       while fs do
 | |
|         local name, t = uv.fs_scandir_next(fs)
 | |
|         if not name then
 | |
|           break
 | |
|         end
 | |
|         local f = level == 1 and name or M.joinpath(dir0, name)
 | |
|         coroutine.yield(f, t)
 | |
|         if
 | |
|           opts.depth
 | |
|           and level < opts.depth
 | |
|           and (t == 'directory' or (t == 'link' and opts.follow and (
 | |
|             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 }
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end)
 | |
| end
 | |
| 
 | |
| --- @class vim.fs.find.Opts
 | |
| --- @inlinedoc
 | |
| ---
 | |
| --- Path to begin searching from. If
 | |
| --- omitted, the |current-directory| is used.
 | |
| --- @field path? string
 | |
| ---
 | |
| --- Search upward through parent directories.
 | |
| --- Otherwise, search through child directories (recursively).
 | |
| --- (default: `false`)
 | |
| --- @field upward? boolean
 | |
| ---
 | |
| --- Stop searching when this directory is reached.
 | |
| --- The directory itself is not searched.
 | |
| --- @field stop? string
 | |
| ---
 | |
| --- Find only items of the given type.
 | |
| --- If omitted, all items that match {names} are included.
 | |
| --- @field type? string
 | |
| ---
 | |
| --- Stop the search after finding this many matches.
 | |
| --- Use `math.huge` to place no limit on the number of matches.
 | |
| --- (default: `1`)
 | |
| --- @field limit? number
 | |
| ---
 | |
| --- Follow symbolic links.
 | |
| --- (default: `false`)
 | |
| --- @field follow? boolean
 | |
| 
 | |
| --- 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.
 | |
| ---
 | |
| --- Examples:
 | |
| ---
 | |
| --- ```lua
 | |
| --- -- List all test directories under the runtime directory.
 | |
| --- local dirs = vim.fs.find(
 | |
| ---   { 'test', 'tst', 'testdir' },
 | |
| ---   { limit = math.huge, type = 'directory', path = './runtime/' }
 | |
| --- )
 | |
| ---
 | |
| --- -- Get all "lib/*.cpp" and "lib/*.hpp" files, using Lua patterns.
 | |
| --- -- Or use `vim.glob.to_lpeg(…):match(…)` for glob/wildcard matching.
 | |
| --- local files = vim.fs.find(function(name, path)
 | |
| ---   return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$')
 | |
| --- end, { limit = math.huge, type = 'file' })
 | |
| --- ```
 | |
| ---
 | |
| ---@since 10
 | |
| ---@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? vim.fs.find.Opts Optional keyword arguments:
 | |
| ---@return (string[]) # Normalized paths |vim.fs.normalize()| of all matching items
 | |
| function M.find(names, opts)
 | |
|   opts = opts or {}
 | |
|   vim.validate('names', names, { 'string', 'table', 'function' })
 | |
|   vim.validate('path', opts.path, 'string', true)
 | |
|   vim.validate('upward', opts.upward, 'boolean', true)
 | |
|   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 }
 | |
|   end
 | |
| 
 | |
|   local path = opts.path or assert(uv.cwd())
 | |
|   local stop = opts.stop
 | |
|   local limit = opts.limit or 1
 | |
| 
 | |
|   local matches = {} --- @type string[]
 | |
| 
 | |
|   local function add(match)
 | |
|     matches[#matches + 1] = M.normalize(match)
 | |
|     if #matches == limit then
 | |
|       return true
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   if opts.upward then
 | |
|     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 (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 = {} --- @type string[]
 | |
|         for _, name in ipairs(names) do
 | |
|           local f = M.joinpath(p, name)
 | |
|           local stat = uv.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
 | |
| 
 | |
|     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
 | |
| 
 | |
|       for other, type_ in M.dir(dir) do
 | |
|         local f = M.joinpath(dir, other)
 | |
|         if type(names) == 'function' then
 | |
|           if (not opts.type or opts.type == type_) and names(other, dir) then
 | |
|             if add(f) then
 | |
|               return matches
 | |
|             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
 | |
| 
 | |
|         if
 | |
|           type_ == 'directory'
 | |
|           or (type_ == 'link' and opts.follow and (uv.fs_stat(f) or {}).type == 'directory')
 | |
|         then
 | |
|           dirs[#dirs + 1] = f
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   return matches
 | |
| end
 | |
| 
 | |
| --- Find the first parent directory containing a specific "marker", relative to a file path or
 | |
| --- buffer.
 | |
| ---
 | |
| --- If the buffer is unnamed (has no backing file) or has a non-empty 'buftype' then the search
 | |
| --- begins from Nvim's |current-directory|.
 | |
| ---
 | |
| --- Examples:
 | |
| ---
 | |
| --- ```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)
 | |
| ---
 | |
| --- -- Find the first ancestor directory containing EITHER "stylua.toml" or ".luarc.json"; if
 | |
| --- -- not found, find the first ancestor containing ".git":
 | |
| --- vim.fs.root(0, { { 'stylua.toml', '.luarc.json' }, '.git' })
 | |
| --- ```
 | |
| ---
 | |
| --- @since 12
 | |
| --- @param source integer|string Buffer number (0 for current buffer) or file path (absolute or
 | |
| ---               relative to the |current-directory|) to begin the search from.
 | |
| --- @param marker (string|string[]|fun(name: string, path: string): boolean)[]|string|fun(name: string, path: string): boolean
 | |
| ---               Filename, function, or list thereof, that decides how to find the root. To
 | |
| ---               indicate "equal priority", specify items in a nested list `{ { 'a.txt', 'b.lua' }, … }`.
 | |
| ---               A function item must return true if `name` and `path` are a match. Each item
 | |
| ---               (which may itself be a nested list) is evaluated in-order against all ancestors,
 | |
| ---               until a match is found.
 | |
| --- @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
 | |
|     if vim.bo[source].buftype ~= '' then
 | |
|       path = assert(uv.cwd())
 | |
|     else
 | |
|       path = vim.api.nvim_buf_get_name(source)
 | |
|     end
 | |
|   else
 | |
|     error('invalid type for argument "source": expected string or buffer number')
 | |
|   end
 | |
| 
 | |
|   local markers = type(marker) == 'table' and marker or { marker }
 | |
|   for _, mark in ipairs(markers) do
 | |
|     local paths = M.find(mark, {
 | |
|       upward = true,
 | |
|       path = vim.fn.fnamemodify(path, ':p:h'),
 | |
|     })
 | |
| 
 | |
|     if #paths ~= 0 then
 | |
|       return vim.fs.dirname(paths[1])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   return nil
 | |
| 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.
 | |
| ---
 | |
| --- 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, '/')
 | |
|   local new_path_components = {}
 | |
| 
 | |
|   for component in vim.gsplit(path, '/') 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
 | |
| 
 | |
| --- 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
 | |
| ---
 | |
| --- Expand environment variables.
 | |
| --- (default: `true`)
 | |
| --- @field expand_env? boolean
 | |
| ---
 | |
| --- @field package _fast? 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.
 | |
| ---   - "/../../" => "/"
 | |
| ---
 | |
| --- On Windows, backslash (\) characters are converted to forward slashes (/).
 | |
| ---
 | |
| --- Examples:
 | |
| --- ```lua
 | |
| --- [[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"
 | |
| --- ```
 | |
| ---
 | |
| ---@since 10
 | |
| ---@param path (string) Path to normalize
 | |
| ---@param opts? vim.fs.normalize.Opts
 | |
| ---@return (string) : Normalized path
 | |
| function M.normalize(path, opts)
 | |
|   opts = opts or {}
 | |
| 
 | |
|   if not opts._fast then
 | |
|     vim.validate('path', path, 'string')
 | |
|     vim.validate('expand_env', opts.expand_env, 'boolean', true)
 | |
|     vim.validate('win', opts.win, 'boolean', true)
 | |
|   end
 | |
| 
 | |
|   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
 | |
| 
 | |
|   -- 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) --- @type string
 | |
|   end
 | |
| 
 | |
|   if win then
 | |
|     -- Convert path separator to `/`
 | |
|     path = path:gsub(os_sep_local, '/')
 | |
|   end
 | |
| 
 | |
|   -- Check for double slashes at the start of the path because they have special meaning
 | |
|   local double_slash = false
 | |
|   if not opts._fast then
 | |
|     double_slash = vim.startswith(path, '//') and not vim.startswith(path, '///')
 | |
|   end
 | |
| 
 | |
|   local prefix = ''
 | |
| 
 | |
|   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
 | |
| 
 | |
|     -- Ensure capital drive and remove extraneous slashes from the prefix
 | |
|     prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/')
 | |
|   end
 | |
| 
 | |
|   if not opts._fast then
 | |
|     -- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix
 | |
|     -- and path.
 | |
|     path = path_resolve_dot(path)
 | |
|   end
 | |
| 
 | |
|   -- 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
 | |
| 
 | |
|   -- Change empty path to `.`
 | |
|   if path == '' then
 | |
|     path = '.'
 | |
|   end
 | |
| 
 | |
|   return path
 | |
| end
 | |
| 
 | |
| --- @param path string Path to remove
 | |
| --- @param ty string type of path
 | |
| --- @param recursive? boolean
 | |
| --- @param force? boolean
 | |
| local function rm(path, ty, recursive, force)
 | |
|   --- @diagnostic disable-next-line:no-unknown
 | |
|   local rm_fn
 | |
| 
 | |
|   if ty == 'directory' then
 | |
|     if recursive then
 | |
|       for file, fty in vim.fs.dir(path) do
 | |
|         rm(M.joinpath(path, file), fty, true, force)
 | |
|       end
 | |
|     elseif not force then
 | |
|       error(string.format('%s is a directory', path))
 | |
|     end
 | |
| 
 | |
|     rm_fn = uv.fs_rmdir
 | |
|   else
 | |
|     rm_fn = uv.fs_unlink
 | |
|   end
 | |
| 
 | |
|   local ret, err, errnm = rm_fn(path)
 | |
|   if ret == nil and (not force or errnm ~= 'ENOENT') then
 | |
|     error(err)
 | |
|   end
 | |
| end
 | |
| 
 | |
| --- @class vim.fs.rm.Opts
 | |
| --- @inlinedoc
 | |
| ---
 | |
| --- Remove directories and their contents recursively
 | |
| --- @field recursive? boolean
 | |
| ---
 | |
| --- Ignore nonexistent files and arguments
 | |
| --- @field force? boolean
 | |
| 
 | |
| --- Remove files or directories
 | |
| --- @since 13
 | |
| --- @param path string Path to remove
 | |
| --- @param opts? vim.fs.rm.Opts
 | |
| function M.rm(path, opts)
 | |
|   opts = opts or {}
 | |
| 
 | |
|   local stat, err, errnm = uv.fs_stat(path)
 | |
|   if stat then
 | |
|     rm(path, stat.type, opts.recursive, opts.force)
 | |
|   elseif not opts.force or errnm ~= 'ENOENT' then
 | |
|     error(err)
 | |
|   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
 | 
