Files
neovim/test/functional/core/path_spec.lua
tao 4a08596314 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>
2026-07-12 13:23:44 -04:00

264 lines
7.7 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local clear = n.clear
local command = n.command
local eq = t.eq
local eval = n.eval
local feed = n.feed
local fn = n.fn
local insert = n.insert
local is_os = t.is_os
local mkdir = t.mkdir
local rmdir = n.rmdir
local write_file = t.write_file
local api = n.api
local function join_path(...)
return table.concat({ ... }, '/')
end
describe('path', function()
local targetdir
local expected_path
before_each(function()
targetdir = join_path('test', 'functional', 'fixtures')
clear()
command('edit ' .. join_path(targetdir, 'tty-test.c'))
expected_path = eval('expand("%:p")')
end)
it('with /./ segment #7117', function()
command('edit ' .. join_path(targetdir, '.', 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with ./ prefix #7117', function()
command('edit ' .. join_path('.', targetdir, 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with ./ prefix, after directory change #7117', function()
command('edit ' .. join_path('.', targetdir, 'tty-test.c'))
command('cd test')
eq(expected_path, eval('expand("%:p")'))
end)
it('with /../ segment #7117', function()
command('edit ' .. join_path(targetdir, '..', 'fixtures', 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with ../ and different starting directory #7117', function()
command('cd test')
command('edit ' .. join_path('..', targetdir, 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with ./../ and different starting directory #7117', function()
command('cd test')
command('edit ' .. join_path('.', '..', targetdir, 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with implicit drive letter #40013', function()
t.skip(not is_os('win'), 'N/A: only works on Windows')
command('edit ' .. expected_path:sub(3))
eq(1, #fn.getbufinfo())
eq(expected_path, eval('expand("%:p")'))
end)
end)
describe('startup', function()
local home, foo, bar = vim.fs.normalize('~'), 'Xtest-foo.lua', 'Xtest-bar.lua'
setup(function()
write_file(('%s/%s'):format(home, foo), 'local foo = 1;')
write_file(('%s/%s'):format(home, bar), '')
end)
teardown(function()
os.remove(('%s/%s'):format(home, foo))
os.remove(('%s/%s'):format(home, bar))
end)
it('expands tilde-prefixed paths #29380', function()
t.skip(not is_os('win'), 'N/A: shell expands ~ on Unix')
clear {
args = { ('~/%s'):format(foo), ('~\\%s'):format(bar) },
}
local expected = { ('%s/%s'):format(home, foo), ('%s/%s'):format(home, bar) }
eq(expected, vim.tbl_map(fn.bufname, api.nvim_list_bufs()))
eq(expected, fn.argv())
end)
it('normalizes v:progpath #39382', function()
clear()
eq(
n.nvim_prog,
fn.system({
is_os('win') and n.nvim_prog:gsub('/', '\\') or n.nvim_prog,
'--clean',
'--headless',
'+echo v:progpath',
'+q',
})
)
end)
it('normalizes -u arguments and triggers autocmd events #39382', function()
local file = ('%s/%s'):format(home, foo)
if is_os('win') then
file = file:gsub('/', '\\')
end
clear {
args_rm = { '--cmd', '-u' },
args = {
'--cmd',
('autocmd SourcePre */%s let g:matched = 1'):format(foo),
'-u',
file,
},
}
local scripts = fn.getscriptinfo({ name = foo })
eq(1, #scripts)
eq(('%s/%s'):format(home, foo), scripts[1].name)
t.ok(fn.eval('g:matched'))
end)
end)
describe('expand wildcard', function()
before_each(clear)
it('with special characters #24421', function()
local folders = is_os('win') and {
'{folder}',
'folder$name',
} or {
'folder-name',
'folder#name',
}
for _, folder in ipairs(folders) do
mkdir(folder)
local file = ('%s%sfile.txt'):format(folder, n.get_pathsep())
write_file(file, '')
eq(file, fn.expand(('%s/*'):format(folder)))
rmdir(folder)
end
end)
end)
describe('file search', function()
local testdir = 'Xtest_path_spec'
before_each(clear)
setup(function()
mkdir(testdir)
end)
teardown(function()
rmdir(testdir)
end)
it('gf finds multibyte filename in line #20517', function()
command('cd test/functional/fixtures')
insert('filename_with_unicode_ααα')
eq('', eval('expand("%")'))
feed('gf')
eq('filename_with_unicode_ααα', eval('expand("%:t")'))
end)
it('gf/<cfile> matches Windows drive-letter filepaths (without ":" in &isfname)', function()
local iswin = is_os('win')
local function test_cfile(input, expected, expected_win)
expected = (iswin and expected_win or expected) or input
command('%delete')
insert(input)
command('norm! 0')
eq(expected, eval('expand("<cfile>")'))
end
-- test_cfile([[c:/d:/e:/foo/bar.txt]], 'c:/d:/e') -- TODO(justinmk): should return "d:/foo/bar.txt" ?
test_cfile([[c:/d:/foo/bar.txt]]) -- TODO(justinmk): should return "d:/foo/bar.txt" ?
test_cfile([[//share/c:/foo/bar/]])
test_cfile([[file://c:/foo/bar]])
test_cfile([[file://c:/foo/bar:42]])
test_cfile([[file://c:/foo/bar:42:666]])
test_cfile([[https://c:/foo/bar]])
test_cfile([[\foo\bar]], [[foo]], [[\foo\bar]])
test_cfile([[/foo/bar]], [[/foo/bar]])
test_cfile([[c:\foo\bar]], [[c:]], [[c:\foo\bar]])
test_cfile([[c:\foo\bar:42:666]], [[c:]], [[c:\foo\bar]])
test_cfile([[c:/foo/bar]])
test_cfile([[c:/foo/bar:42]], [[c:/foo/bar]])
test_cfile([[c:/foo/bar:42:666]], [[c:/foo/bar]])
test_cfile([[c:foo\bar]], [[c]])
test_cfile([[c:foo/bar]], [[c]])
test_cfile([[c:foo]], [[c]])
-- Examples from: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#example-ways-to-refer-to-the-same-file
test_cfile([[c:\temp\test-file.txt]], [[c:]], [[c:\temp\test-file.txt]])
test_cfile(
[[\\127.0.0.1\c$\temp\test-file.txt]],
[[127.0.0.1]],
[[\\127.0.0.1\c$\temp\test-file.txt]]
)
test_cfile(
[[\\LOCALHOST\c$\temp\test-file.txt]],
[[LOCALHOST]],
[[\\LOCALHOST\c$\temp\test-file.txt]]
)
-- not supported yet
test_cfile([[\\.\c:\temp\test-file.txt]], [[.]], [[\\.\c]])
-- not supported yet
test_cfile([[\\?\c:\temp\test-file.txt]], [[c:]], [[\\]])
test_cfile(
[[\\.\UNC\LOCALHOST\c$\temp\test-file.txt]],
[[.]],
[[\\.\UNC\LOCALHOST\c$\temp\test-file.txt]]
)
test_cfile(
[[\\127.0.0.1\c$\temp\test-file.txt]],
[[127.0.0.1]],
[[\\127.0.0.1\c$\temp\test-file.txt]]
)
end)
it('gf/<cfile> handles local file: paths', function()
local path = fn.fnamemodify(join_path(testdir, 'gf-target.txt'), ':p'):gsub('\\', '/')
local uri = 'file:' .. (is_os('win') and '/' or '') .. path
write_file(path, '')
insert(uri)
command('norm! 0')
eq(path, eval('expand("<cfile>")'))
feed('gf')
eq(path, fn.fnamemodify(eval('expand("%:p")'), ':p'):gsub('\\', '/'))
end)
---@param funcname 'finddir' | 'findfile'
local function test_find_func(funcname, folder, item)
local d = join_path(testdir, folder)
mkdir(d)
local expected = join_path(d, item)
if funcname == 'finddir' then
mkdir(expected)
else
write_file(expected, '')
end
eq(expected, fn[funcname](item, d:gsub(' ', [[\ ]])))
end
it('finddir()', function()
test_find_func('finddir', 'directory', 'folder')
test_find_func('finddir', 'directory', 'folder name')
test_find_func('finddir', 'fold#er name', 'directory')
end)
it('findfile()', function()
test_find_func('findfile', 'directory', 'file.txt')
test_find_func('findfile', 'directory', 'file name.txt')
test_find_func('findfile', 'fold#er name', 'file.txt')
end)
end)