fix(path): normalize separators (slashes) #39155

Problem:
Continue separators normalization, and try to keep it at the
nvim <-> external boundary, e.g., `fn.xxx`, `api.xxx`, `:xxx`

Solution:
some key changes
- normalize `$HOME-windows`
- normalize entry points of changing directory
  - `nvim_set_current_dir()`, `chdir()`
- normalize the named pipe
  - `--listen` arg, `--server` arg, `:restart`
  - `serverstart()`, `sockconnect()`, `serverstop()`
- make `expand()` respect 'shellslash' again
- clean up `did_set_shellslash`
- replace `forward_slash` with `TO_SLASH`
- remove obsolete `TMP_PATHSEPSTR`
- remove `slash_adjust` in `do_autocmd_dirchanged`?
- make `fnamemodify()` always return `/` (except when substituting
  separators via `:s`, `:gs` ?)

Note:
- these funcs still apply `slash_adjust` on return, as before:
  `:pwd`, `chdir()`, `exepath()`, `getcwd()`
- these funcs alwarys return `/`, unlike before the normalization PRs
  - `getcompletion()`, `finddir()`, `findfile()`

Also
- clean up code, comments, formatting and tests
- move `TO_SLASH` from `f_chdir` to `changedir_func`
- move `TO_SLASH` from `f_bufadd` to `buflist_new`

Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
This commit is contained in:
tao
2026-07-13 01:23:44 +08:00
committed by GitHub
parent 543a96e491
commit 4a08596314
51 changed files with 802 additions and 853 deletions

View File

@@ -322,7 +322,7 @@ describe('context functions', function()
filter(map(add(
getjumplist()[0], { 'bufnr': bufnr('%'), 'lnum': getcurpos()[1] }),
'filter(
{ "f": expand("#".v:val.bufnr.":p"), "l": v:val.lnum },
{ "f": expand("#".v:val.bufnr.":p:gs?\\?/?"), "l": v:val.lnum },
{ k, v -> k != "l" || v != 1 })'), '!empty(v:val.f)')
]]):gsub('\n', ''))),
}

View File

@@ -2,7 +2,7 @@ local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local eq, clear, call, write_file, command = t.eq, n.clear, n.call, t.write_file, n.command
local eval = n.eval
local eval, fn = n.eval, n.fn
local is_os = t.is_os
local pcall_err = t.pcall_err
@@ -18,38 +18,26 @@ describe('executable()', function()
eq(1, call('executable', 'false'))
end)
if is_os('win') then
it('exepath returns consistent slashes #13787', function()
-- test/ cannot be a symlink in this test.
n.api.nvim_set_current_dir(t.paths.test_source_path)
it('exepath respects shellslash #13787', function()
t.skip(not is_os('win'), 'N/A for non-Windows')
-- test/ cannot be a symlink in this test.
n.api.nvim_set_current_dir(t.paths.test_source_path)
command('let $PATH = fnamemodify("./test/functional/fixtures/bin", ":p")')
eq(
[[test/functional/fixtures/bin/null.CMD]],
call('fnamemodify', call('exepath', 'null'), ':.')
)
command('set shellslash')
eq(
'test/functional/fixtures/bin/null.CMD',
call('fnamemodify', call('exepath', 'null'), ':.')
)
end)
command('let $PATH = fnamemodify("./test/functional/fixtures/bin", ":p")')
eq(([[%s\test\functional\fixtures\bin\null.CMD]]):format(fn.getcwd()), fn.exepath('null'))
command('set shellslash')
eq(('%s/test/functional/fixtures/bin/null.CMD'):format(fn.getcwd()), fn.exepath('null'))
end)
it('stdpath returns consistent slashes #13787', function()
-- Needs to check paths relative to repo root dir.
n.api.nvim_set_current_dir(t.paths.test_source_path)
it('stdpath returns consistent slashes #13787', function()
t.skip(not is_os('win'), 'N/A for non-Windows')
-- Needs to check paths relative to repo root dir.
n.api.nvim_set_current_dir(t.paths.test_source_path)
t.matches(
[[build/Xtest_xdg[%w_]*/share/nvim%-data]],
call('fnamemodify', call('stdpath', 'data'), ':.')
)
command('set shellslash')
t.matches(
'build/Xtest_xdg[%w_]*/share/nvim%-data',
call('fnamemodify', call('stdpath', 'data'), ':.')
)
end)
end
t.matches('build/Xtest_xdg[%w_]*/share/nvim%-data', fn.stdpath('data'))
command('set shellslash')
t.matches('build/Xtest_xdg[%w_]*/share/nvim%-data', fn.stdpath('data'))
end)
it('fails for invalid values', function()
for _, input in ipairs({ 'v:null', 'v:true', 'v:false', '{}', '[]' }) do

View File

@@ -10,10 +10,6 @@ local write_file = t.write_file
local is_os = t.is_os
local chdir = n.fn.chdir
local function eq_slashconvert(expected, got)
eq(t.fix_slashes(expected), t.fix_slashes(got))
end
describe('fnamemodify()', function()
setup(function()
write_file('Xtest-fnamemodify.txt', [[foobar]])
@@ -42,7 +38,7 @@ describe('fnamemodify()', function()
eq(root, fnamemodify([[/]], ':p'))
local letter_colon = root:sub(1, 2)
local old_dir = t.fix_slashes(getcwd()) .. '/'
local old_dir = ('%s/'):format(getcwd())
local foo_dir = old_dir .. 'foo/'
eq(old_dir, fnamemodify(letter_colon, ':p'))
eq(old_dir, fnamemodify(letter_colon .. '.', ':p'))
@@ -65,28 +61,28 @@ describe('fnamemodify()', function()
n.api.nvim_set_current_dir(t.paths.test_source_path)
local filename = 'src/version.c'
local cwd = getcwd()
local cwd = vim.fs.normalize(getcwd())
eq_slashconvert(cwd .. '/src/version.c', fnamemodify(filename, ':p'))
eq(('%s/src/version.c'):format(cwd), fnamemodify(filename, ':p'))
eq_slashconvert('src/version.c', fnamemodify(filename, ':p:.'))
eq_slashconvert(cwd .. '/src', fnamemodify(filename, ':p:h'))
eq_slashconvert(cwd .. '', fnamemodify(filename, ':p:h:h'))
eq('src/version.c', fnamemodify(filename, ':p:.'))
eq(('%s/src'):format(cwd), fnamemodify(filename, ':p:h'))
eq(cwd, fnamemodify(filename, ':p:h:h'))
eq('version.c', fnamemodify(filename, ':p:t'))
eq_slashconvert(cwd .. '/src/version', fnamemodify(filename, ':p:r'))
eq(('%s/src/version'):format(cwd), fnamemodify(filename, ':p:r'))
eq_slashconvert(cwd .. '/src/main.c', fnamemodify(filename, ':s?version?main?:p'))
eq(('%s/src/main.c'):format(cwd), fnamemodify(filename, ':s?version?main?:p'))
local converted_cwd = cwd:gsub('/', '\\')
eq(converted_cwd .. '\\src\\version.c', fnamemodify(filename, ':p:gs?/?\\\\?'))
eq('src', fnamemodify(filename, ':h'))
eq('version.c', fnamemodify(filename, ':t'))
eq_slashconvert('src/version', fnamemodify(filename, ':r'))
eq('src/version', fnamemodify(filename, ':r'))
eq('version', fnamemodify(filename, ':t:r'))
eq('c', fnamemodify(filename, ':e'))
eq_slashconvert('src/main.c', fnamemodify(filename, ':s?version?main?'))
eq('src/main.c', fnamemodify(filename, ':s?version?main?'))
end)
it('handles advanced examples from ":help filename-modifiers"', function()
@@ -98,11 +94,11 @@ describe('fnamemodify()', function()
eq('c', fnamemodify(filename, ':e:e:r'))
eq_slashconvert('src/version.c', fnamemodify(filename, ':r'))
eq('src/version.c', fnamemodify(filename, ':r'))
eq('c', fnamemodify(filename, ':r:e'))
eq_slashconvert('src/version', fnamemodify(filename, ':r:r'))
eq_slashconvert('src/version', fnamemodify(filename, ':r:r:r'))
eq('src/version', fnamemodify(filename, ':r:r'))
eq('src/version', fnamemodify(filename, ':r:r:r'))
end)
it('handles :h', function()
@@ -136,11 +132,11 @@ describe('fnamemodify()', function()
eq('//server///share', fnamemodify('//server///share', ':h'))
eq('//server/share/', fnamemodify('//server/share/foo', ':h'))
eq([[\\foo\C$]], fnamemodify([[\\foo\C$]], ':h'))
eq([[\\foo\C$\]], fnamemodify([[\\foo\C$\]], ':h'))
eq([[\\foo\C$\]], fnamemodify([[\\foo\C$\bar]], ':h'))
eq([[\\foo\C$\]], fnamemodify([[\\foo\C$\bar]], ':h:h'))
eq([[//foo\C$/]], fnamemodify([[//foo\C$/bar]], ':h'))
eq([[//foo/C$]], fnamemodify([[\\foo\C$]], ':h'))
eq([[//foo/C$/]], fnamemodify([[\\foo\C$\]], ':h'))
eq([[//foo/C$/]], fnamemodify([[\\foo\C$\bar]], ':h'))
eq([[//foo/C$/]], fnamemodify([[\\foo\C$\bar]], ':h:h'))
eq([[//foo/C$/]], fnamemodify([[//foo\C$/bar]], ':h'))
-- `C$` is a share name, not a file name
eq('', fnamemodify('//foo/C$/bar', ':h:t'))
@@ -165,17 +161,17 @@ describe('fnamemodify()', function()
it('handles :t', function()
eq('hello.txt', fnamemodify('hello.txt', ':t'))
eq_slashconvert('hello.txt', fnamemodify('path/to/hello.txt', ':t'))
eq('hello.txt', fnamemodify('path/to/hello.txt', ':t'))
end)
it('handles :r', function()
eq('hello', fnamemodify('hello.txt', ':r'))
eq_slashconvert('path/to/hello', fnamemodify('path/to/hello.txt', ':r'))
eq('path/to/hello', fnamemodify('path/to/hello.txt', ':r'))
end)
it('handles :e', function()
eq('txt', fnamemodify('hello.txt', ':e'))
eq_slashconvert('txt', fnamemodify('path/to/hello.txt', ':e'))
eq('txt', fnamemodify('path/to/hello.txt', ':e'))
end)
it('handles regex replacements', function()