From 19715e6e8a7145c8d4da39c9ebc2b09da5be74e7 Mon Sep 17 00:00:00 2001 From: tao <2471314@gmail.com> Date: Thu, 19 Mar 2026 07:54:19 +0800 Subject: [PATCH] fix(fs): expand drive-relative paths on Windows #37084 Problem: On windows, if a drive-relative path doesn't contain a slash, `path_to_absolute` can't split out the relative component, causing expansion to fails. e.g., `c:` `c:.` `c:..` `c:foo.md` Solution: For these cases, we can pass letter and colon to `path_full_dir_name`. Notably, `..` is included as well. if that relative path exists, it can be expanded correctly. --- src/nvim/path.c | 7 +++++-- test/functional/vimscript/fnamemodify_spec.lua | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/nvim/path.c b/src/nvim/path.c index 298026d7b3..fe8fe651d8 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -2380,20 +2380,23 @@ static int path_to_absolute(const char *fname, char *buf, size_t len, int force) if (p == NULL) { p = strrchr(fname, '\\'); } + if (p == NULL && ASCII_ISALPHA(fname[0]) && fname[1] == ':') { + p = fname + 1; + } #endif if (p == NULL && strcmp(fname, "..") == 0) { // Handle ".." without path separators. p = fname + 2; } if (p != NULL) { - if (vim_ispathsep_nocolon(*p) && strcmp(p + 1, "..") == 0) { + if (vim_ispathsep(*p) && strcmp(p + 1, "..") == 0) { // For "/path/dir/.." include the "/..". p += 3; } assert(p >= fname); memcpy(relative_directory, fname, (size_t)(p - fname + 1)); relative_directory[p - fname + 1] = NUL; - end_of_path = (vim_ispathsep_nocolon(*p) ? p + 1 : p); + end_of_path = (vim_ispathsep(*p) ? p + 1 : p); } else { relative_directory[0] = NUL; } diff --git a/test/functional/vimscript/fnamemodify_spec.lua b/test/functional/vimscript/fnamemodify_spec.lua index 8b0b242c62..380471320e 100644 --- a/test/functional/vimscript/fnamemodify_spec.lua +++ b/test/functional/vimscript/fnamemodify_spec.lua @@ -8,6 +8,7 @@ local getcwd = n.fn.getcwd local command = n.command 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)) @@ -16,12 +17,15 @@ end describe('fnamemodify()', function() setup(function() write_file('Xtest-fnamemodify.txt', [[foobar]]) + t.mkdir('foo') + write_file('foo/bar', [[bar]]) end) before_each(clear) teardown(function() os.remove('Xtest-fnamemodify.txt') + n.rmdir('foo') end) it('handles the root path', function() @@ -37,6 +41,19 @@ describe('fnamemodify()', function() eq(root, fnamemodify([[\]], ':p')) eq(root, fnamemodify([[/]], ':p:h')) eq(root, fnamemodify([[/]], ':p')) + + local letter_colon = root:sub(1, 2) + local old_dir = getcwd() .. '/' + local foo_dir = old_dir .. 'foo/' + eq(old_dir, fnamemodify(letter_colon, ':p')) + eq(old_dir, fnamemodify(letter_colon .. '.', ':p')) + eq(old_dir, fnamemodify(letter_colon .. './', ':p')) + eq(foo_dir, fnamemodify(letter_colon .. './foo', ':p')) + eq(foo_dir, fnamemodify(letter_colon .. 'foo', ':p')) + chdir('foo') + eq(old_dir, fnamemodify(letter_colon .. '..', ':p')) + eq(old_dir, fnamemodify(letter_colon .. '../', ':p')) + eq(foo_dir .. 'bar', fnamemodify(letter_colon .. 'bar', ':p')) end end)