From 38e38d1b401e38459842f0e4da431e3dd6c2e527 Mon Sep 17 00:00:00 2001 From: James Trew <66286082+jamestrew@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:23:01 -0400 Subject: fix(fs): allow backslash characters in unix paths Backslashes are valid characters in unix style paths. Fix the conversion of backslashes to forward slashes in several `vim.fs` functions when not on Windows. On Windows, backslashes will still be converted to forward slashes. --- runtime/lua/vim/fs.lua | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) (limited to 'runtime/lua/vim/fs.lua') diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index f9fe122f01..b7718ac87a 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -1,6 +1,7 @@ local M = {} local iswin = vim.uv.os_uname().sysname == 'Windows_NT' +local os_sep = iswin and '\\' or '/' --- Iterate over all the parents of the given path. --- @@ -47,19 +48,23 @@ function M.dirname(file) return nil end vim.validate({ file = { file, 's' } }) - if iswin and file:match('^%w:[\\/]?$') then - return (file:gsub('\\', '/')) - elseif not file:match('[\\/]') then + 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('^([/\\]?.+)[/\\]') + 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:gsub('\\', '/')) + return dir end --- Return the basename of the given path @@ -72,10 +77,13 @@ function M.basename(file) return nil end vim.validate({ file = { file, 's' } }) - if iswin and file:match('^%w:[\\/]?$') then - return '' + 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('[^\\/]*$'):gsub('\\', '/')) + return file:match('/$') and '' or (file:match('[^/]*$')) end --- Concatenate directories and/or file paths into a single path with normalization @@ -334,15 +342,16 @@ end --- @field expand_env 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 any ---- backslash (\) characters are converted to forward slashes (/). Environment ---- variables are also expanded. +--- beginning of the path is expanded to the user's home directory and +--- environment variables are also expanded. +--- +--- On Windows, backslash (\) characters are converted to forward slashes (/). --- --- Examples: --- --- ```lua --- vim.fs.normalize('C:\\\\Users\\\\jdoe') ---- -- 'C:/Users/jdoe' +--- -- On Windows: 'C:/Users/jdoe' --- --- vim.fs.normalize('~/src/neovim') --- -- '/home/jdoe/src/neovim' @@ -364,7 +373,7 @@ function M.normalize(path, opts) if path:sub(1, 1) == '~' then local home = vim.uv.os_homedir() or '~' - if home:sub(-1) == '\\' or home:sub(-1) == '/' then + if home:sub(-1) == os_sep then home = home:sub(1, -2) end path = home .. path:sub(2) @@ -374,7 +383,7 @@ function M.normalize(path, opts) path = path:gsub('%$([%w_]+)', vim.uv.os_getenv) end - path = path:gsub('\\', '/'):gsub('/+', '/') + path = path:gsub(os_sep, '/'):gsub('/+', '/') if iswin and path:match('^%w:/$') then return path end -- cgit From 2424c3e6967ef953222cf48564629ffe5812d415 Mon Sep 17 00:00:00 2001 From: dundargoc Date: Fri, 29 Mar 2024 18:05:02 +0100 Subject: fix: support UNC paths in vim.fs.normalize Closes https://github.com/neovim/neovim/issues/27068. --- runtime/lua/vim/fs.lua | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/fs.lua') diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index b7718ac87a..ad0d914ea2 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -371,7 +371,8 @@ function M.normalize(path, opts) expand_env = { opts.expand_env, { 'boolean' }, true }, }) - if path:sub(1, 1) == '~' then + -- Expand ~ to users home directory + if vim.startswith(path, '~') then local home = vim.uv.os_homedir() or '~' if home:sub(-1) == os_sep then home = home:sub(1, -2) @@ -379,15 +380,32 @@ function M.normalize(path, opts) path = home .. path:sub(2) end + -- Expand environment variables if `opts.expand_env` isn't `false` if opts.expand_env == nil or opts.expand_env then path = path:gsub('%$([%w_]+)', vim.uv.os_getenv) end - path = path:gsub(os_sep, '/'):gsub('/+', '/') + -- Convert path separator to `/` + path = path:gsub(os_sep, '/') + + -- Don't modify leading double slash as those have implementation-defined behavior according to + -- POSIX. They are also valid UNC paths. Three or more leading slashes are however collapsed to + -- a single slash. + if vim.startswith(path, '//') and not vim.startswith(path, '///') then + path = '/' .. path:gsub('/+', '/') + else + path = path:gsub('/+', '/') + end + + -- Ensure last slash is not truncated from root drive on Windows if iswin and path:match('^%w:/$') then return path end - return (path:gsub('(.)/$', '%1')) + + -- Remove trailing slashes + path = path:gsub('(.)/$', '%1') + + return path end return M -- cgit From 8e5c48b08dad54706500e353c58ffb91f2684dd3 Mon Sep 17 00:00:00 2001 From: Famiu Haque Date: Wed, 17 Apr 2024 01:13:44 +0600 Subject: feat(lua): vim.fs.normalize() resolves ".", ".." #28203 Problem: `vim.fs.normalize` does not resolve `.` and `..` components. This makes no sense as the entire point of normalization is to remove redundancy from the path. The path normalization functions in several other languages (Java, Python, C++, etc.) also resolve `.` and `..` components. Reference: - Python: https://docs.python.org/3/library/os.path.html#os.path.normpath - Java: https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html#normalize-- - C++: https://en.cppreference.com/w/cpp/filesystem/path/lexically_normal Solution: Resolve "." and ".." in `vim.fs.normalize`. Before: "~/foo/bar/../baz/./" => "~/foo/bar/../baz/." After: "~/foo/bar/../baz/./" => "~/foo/baz" --- runtime/lua/vim/fs.lua | 195 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 166 insertions(+), 29 deletions(-) (limited to 'runtime/lua/vim/fs.lua') diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index ad0d914ea2..65ad58c720 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -334,30 +334,147 @@ function M.find(names, opts) return matches 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, '/') + -- 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 + --- @class vim.fs.normalize.Opts --- @inlinedoc --- --- Expand environment variables. --- (default: `true`) ---- @field expand_env 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. +--- @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. +--- - "/../../" => "/" --- --- On Windows, backslash (\) characters are converted to forward slashes (/). --- --- Examples: ---- --- ```lua ---- vim.fs.normalize('C:\\\\Users\\\\jdoe') ---- -- On Windows: 'C:/Users/jdoe' ---- ---- vim.fs.normalize('~/src/neovim') ---- -- '/home/jdoe/src/neovim' ---- ---- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim') ---- -- '/Users/jdoe/.config/nvim/init.vim' +--- [[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" --- ``` --- ---@param path (string) Path to normalize @@ -369,12 +486,21 @@ function M.normalize(path, opts) vim.validate({ path = { path, { 'string' } }, expand_env = { opts.expand_env, { 'boolean' }, true }, + win = { opts.win, { 'boolean' }, true }, }) + 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 users home directory if vim.startswith(path, '~') then local home = vim.uv.os_homedir() or '~' - if home:sub(-1) == os_sep then + if home:sub(-1) == os_sep_local then home = home:sub(1, -2) end path = home .. path:sub(2) @@ -386,24 +512,35 @@ function M.normalize(path, opts) end -- Convert path separator to `/` - path = path:gsub(os_sep, '/') + path = path:gsub(os_sep_local, '/') - -- Don't modify leading double slash as those have implementation-defined behavior according to - -- POSIX. They are also valid UNC paths. Three or more leading slashes are however collapsed to - -- a single slash. - if vim.startswith(path, '//') and not vim.startswith(path, '///') then - path = '/' .. path:gsub('/+', '/') - else - path = path:gsub('/+', '/') - end + -- 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 = '' - -- Ensure last slash is not truncated from root drive on Windows - if iswin and path:match('^%w:/$') then - return path + 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('/+', '/') end - -- Remove trailing slashes - path = path:gsub('(.)/$', '%1') + -- 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 return path end -- cgit From 38b9c322c97b63f53caef7a651211fc9312d055e Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:43:46 -0500 Subject: feat(fs): add vim.fs.root (#28477) vim.fs.root() is a function for finding a project root relative to a buffer using one or more "root markers". This is useful for LSP and could be useful for other "projects" designs, as well as for any plugins which work with a "projects" concept. --- runtime/lua/vim/fs.lua | 57 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/fs.lua') diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 65ad58c720..766dc09691 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -197,13 +197,6 @@ end --- 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'}, @@ -334,6 +327,56 @@ function M.find(names, opts) return matches end +--- Find the first parent directory containing a specific "marker", relative to a buffer's +--- directory. +--- +--- Example: +--- +--- ```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) +--- ``` +--- +--- @param source integer|string Buffer number (0 for current buffer) or file path to begin the +--- search from. +--- @param marker (string|string[]|fun(name: string, path: string): boolean) A marker, or list +--- of markers, to search for. If a function, the function is called for each +--- evaluated item and should return true if {name} and {path} are a match. +--- @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 + path = vim.api.nvim_buf_get_name(source) + else + error('invalid type for argument "source": expected string or buffer number') + end + + local paths = M.find(marker, { + upward = true, + path = path, + }) + + if #paths == 0 then + return nil + end + + return vim.fs.dirname(paths[1]) +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. --- -- cgit From 8bb67d64e20d2e0d3e1892cb89d9bb8f558471e4 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Mon, 13 May 2024 17:26:57 +0200 Subject: perf(fs): normalize path only once in fs.dir Re-normalizing a path after a `joinpath` isn't necessary. Calling `normalize` on each child directory had quite a bit of impact when traversing a large directory. A simple test showed: Before: ~144ms After: ~80ms running the following logic against a dir with 4367 child folders and 25826 files: local files = {} local start = uv.hrtime() for name, type in vim.fs.dir(path, { depth = max_depth }) do table.insert(files, { name, type }) end local duration = uv.hrtime() - start Relates to https://github.com/neovim/neovim/issues/23291 --- runtime/lua/vim/fs.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/fs.lua') diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 766dc09691..e70175bcbd 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -120,8 +120,9 @@ function M.dir(path, opts) skip = { opts.skip, { 'function' }, true }, }) + path = M.normalize(path) if not opts.depth or opts.depth == 1 then - local fs = vim.uv.fs_scandir(M.normalize(path)) + local fs = vim.uv.fs_scandir(path) return function() if not fs then return @@ -137,7 +138,7 @@ function M.dir(path, opts) --- @type string, integer local dir0, level = unpack(table.remove(dirs, 1)) local dir = level == 1 and dir0 or M.joinpath(path, dir0) - local fs = vim.uv.fs_scandir(M.normalize(dir)) + local fs = vim.uv.fs_scandir(dir) while fs do local name, t = vim.uv.fs_scandir_next(fs) if not name then -- cgit From dcdefd042840c7a6db5dce2963ac23c45a5287da Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 14 May 2024 17:29:28 +0100 Subject: perf(loader): use a quicker version of vim.fs.normalize Problem: vim.fs.normalize() normalizes too much vim.loader and is slow. Solution: Make it faster by doing less. This reduces the times spent in vim.fs.normalize in vim.loader from ~13ms -> 1-2ms. Numbers from a relative benchmark: - Skipping `vim.validate()`: 285ms -> 230ms - Skipping `path_resolve_dot()`: 285ms -> 60ms - Skipping `double_slash`: 60ms -> 35ms --- runtime/lua/vim/fs.lua | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) (limited to 'runtime/lua/vim/fs.lua') diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index e70175bcbd..58e79db0b4 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -488,6 +488,8 @@ end --- (default: `true`) --- @field expand_env? boolean --- +--- @field package _fast? boolean +--- --- Path is a Windows path. --- (default: `true` in Windows, `false` otherwise) --- @field win? boolean @@ -527,11 +529,13 @@ end function M.normalize(path, opts) opts = opts or {} - vim.validate({ - path = { path, { 'string' } }, - expand_env = { opts.expand_env, { 'boolean' }, true }, - win = { opts.win, { 'boolean' }, true }, - }) + if not opts._fast then + vim.validate({ + path = { path, { 'string' } }, + expand_env = { opts.expand_env, { 'boolean' }, true }, + 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 '/' @@ -555,11 +559,17 @@ function M.normalize(path, opts) path = path:gsub('%$([%w_]+)', vim.uv.os_getenv) end - -- Convert path separator to `/` - path = path:gsub(os_sep_local, '/') + 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 = vim.startswith(path, '//') and not vim.startswith(path, '///') + 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 @@ -576,10 +586,15 @@ function M.normalize(path, opts) prefix = prefix:gsub('/+', '/') end - -- 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 + 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_resolve_dot(path) + path = (double_slash and '/' or '') .. prefix .. path -- Change empty path to `.` if path == '' then -- cgit From 14a5813c207716613daecf4ca9f69e3a3795596a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 15 May 2024 10:19:16 +0100 Subject: perf(vim.fs.normalize): use iterator ~10% faster. --- runtime/lua/vim/fs.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'runtime/lua/vim/fs.lua') diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 58e79db0b4..ce533ad0a9 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -455,11 +455,9 @@ end --- @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 + for component in vim.gsplit(path, '/') do if component == '.' or component == '' then -- luacheck: ignore 542 -- Skip `.` components and empty components elseif component == '..' then -- cgit From 206f8f24a2470f961cfe7e7c177443c0f199231c Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Fri, 24 May 2024 10:48:32 -0500 Subject: fix(fs): make vim.fs.root work for relative paths and unnamed buffers (#28964) If a buffer does not have a backing file then fall back to the current working directory. --- runtime/lua/vim/fs.lua | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/fs.lua') diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index ce533ad0a9..b05220ee2c 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -328,8 +328,11 @@ function M.find(names, opts) return matches end ---- Find the first parent directory containing a specific "marker", relative to a buffer's ---- directory. +--- 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|. --- --- Example: --- @@ -346,13 +349,13 @@ end --- end) --- ``` --- ---- @param source integer|string Buffer number (0 for current buffer) or file path to begin the ---- search from. +--- @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) A marker, or list --- of markers, to search for. If a function, the function is called for each --- evaluated item and should return true if {name} and {path} are a match. --- @return string? # Directory path containing one of the given markers, or nil if no directory was ---- found. +--- found. function M.root(source, marker) assert(source, 'missing required argument: source') assert(marker, 'missing required argument: marker') @@ -361,14 +364,18 @@ function M.root(source, marker) if type(source) == 'string' then path = source elseif type(source) == 'number' then - path = vim.api.nvim_buf_get_name(source) + if vim.bo[source].buftype ~= '' then + path = assert(vim.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 paths = M.find(marker, { upward = true, - path = path, + path = vim.fn.fnamemodify(path, ':p:h'), }) if #paths == 0 then -- cgit