diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index b9bc73e0b8..e02ed20644 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2954,13 +2954,14 @@ vim.fs.joinpath({...}) *vim.fs.joinpath()* vim.fs.normalize({path}, {opts}) *vim.fs.normalize()* 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' 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 diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua index a5cdfdc225..d43f32726d 100644 --- a/test/functional/lua/fs_spec.lua +++ b/test/functional/lua/fs_spec.lua @@ -36,6 +36,7 @@ local test_basename_dirname_eq = { 'c:/users/foo', 'c:/users/foo/bar.lua', 'c:/users/foo/bar/../', + '~/foo/bar\\baz', } local tests_windows_paths = { @@ -70,26 +71,26 @@ describe('vim.fs', function() it('works', function() eq(test_build_dir, vim.fs.dirname(nvim_dir)) - --- @param paths string[] - local function test_paths(paths) + ---@param paths string[] + ---@param is_win? boolean + local function test_paths(paths, is_win) + local gsub = is_win and [[:gsub('\\', '/')]] or '' + local code = string.format( + [[ + local path = ... + return vim.fn.fnamemodify(path,':h')%s + ]], + gsub + ) + for _, path in ipairs(paths) do - eq( - exec_lua( - [[ - local path = ... - return vim.fn.fnamemodify(path,':h'):gsub('\\', '/') - ]], - path - ), - vim.fs.dirname(path), - path - ) + eq(exec_lua(code, path), vim.fs.dirname(path), path) end end test_paths(test_basename_dirname_eq) if is_os('win') then - test_paths(tests_windows_paths) + test_paths(tests_windows_paths, true) end end) end) @@ -98,26 +99,26 @@ describe('vim.fs', function() it('works', function() eq(nvim_prog_basename, vim.fs.basename(nvim_prog)) - --- @param paths string[] - local function test_paths(paths) + ---@param paths string[] + ---@param is_win? boolean + local function test_paths(paths, is_win) + local gsub = is_win and [[:gsub('\\', '/')]] or '' + local code = string.format( + [[ + local path = ... + return vim.fn.fnamemodify(path,':t')%s + ]], + gsub + ) + for _, path in ipairs(paths) do - eq( - exec_lua( - [[ - local path = ... - return vim.fn.fnamemodify(path,':t'):gsub('\\', '/') - ]], - path - ), - vim.fs.basename(path), - path - ) + eq(exec_lua(code, path), vim.fs.basename(path), path) end end test_paths(test_basename_dirname_eq) if is_os('win') then - test_paths(tests_windows_paths) + test_paths(tests_windows_paths, true) end end) end) @@ -284,9 +285,6 @@ describe('vim.fs', function() end) describe('normalize()', function() - it('works with backward slashes', function() - eq('C:/Users/jdoe', vim.fs.normalize('C:\\Users\\jdoe')) - end) it('removes trailing /', function() eq('/home/user', vim.fs.normalize('/home/user/')) end) @@ -309,10 +307,18 @@ describe('vim.fs', function() ) ) end) + if is_os('win') then it('Last slash is not truncated from root drive', function() eq('C:/', vim.fs.normalize('C:/')) end) + it('converts backward slashes', function() + eq('C:/Users/jdoe', vim.fs.normalize('C:\\Users\\jdoe')) + end) + else + it('allows backslashes on unix-based os', function() + eq('/home/user/hello\\world', vim.fs.normalize('/home/user/hello\\world')) + end) end end) end)