From 43bf045005b0fe819e85fdc44d7f7563f8ccda9d Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Mon, 18 Aug 2025 10:33:27 +0800 Subject: [PATCH 1/3] vim-patch:9.1.1646: MS-Windows: completion cannot handle implicit drive letters Problem: MS-Windows: completion cannot handle implicit drive letters Solution: Consider paths like \folder and /folder as absolute (Miguel Barro). closes: vim/vim#17829 https://github.com/vim/vim/commit/a2f13bf782f723e116c5d4cc7d79a23e918a24db Co-authored-by: Miguel Barro --- runtime/doc/news.txt | 2 ++ src/nvim/eval/fs.c | 6 +++- src/nvim/file_search.c | 11 ------ src/nvim/path.c | 52 ++++++++++++++++++++++++++--- test/old/testdir/test_cd.vim | 33 ++++++++++++++++++ test/old/testdir/test_functions.vim | 3 +- 6 files changed, 90 insertions(+), 17 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index bd5288a0e3..78aa02fc3c 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -421,6 +421,8 @@ These existing features changed their behavior. • 'scrollback' maximum value increased from 100000 to 1000000 • |matchfuzzy()| and |matchfuzzypos()| use an improved fuzzy matching algorithm (same as fzy). +- Windows: Paths like "\Windows" and "/Windows" are now considered to be + absolute paths (to the current drive) and no longer relative. ============================================================================== REMOVED FEATURES *news-removed* diff --git a/src/nvim/eval/fs.c b/src/nvim/eval/fs.c index 2b97cebde0..ba6620d82b 100644 --- a/src/nvim/eval/fs.c +++ b/src/nvim/eval/fs.c @@ -111,7 +111,11 @@ repeat: } // FullName_save() is slow, don't use it when not needed. - if (*p != NUL || !vim_isAbsName(*fnamep)) { + if (*p != NUL || !vim_isAbsName(*fnamep) +#ifdef MSWIN // enforce drive letter on windows paths + || **fnamep == '/' || **fnamep == '\\' +#endif + ) { *fnamep = FullName_save(*fnamep, *p != NUL); xfree(*bufp); // free any allocated file name *bufp = *fnamep; diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c index 15658e74e2..bc97b2c482 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -331,17 +331,6 @@ void *vim_findfile_init(char *path, char *filename, size_t filenamelen, char *st ff_expand_buffer.size = strlen(ff_expand_buffer.data); search_ctx->ffsc_start_dir = copy_string(ff_expand_buffer, NULL); - -#ifdef BACKSLASH_IN_FILENAME - // A path that starts with "/dir" is relative to the drive, not to the - // directory (but not for "//machine/dir"). Only use the drive name. - if ((*path == '/' || *path == '\\') - && path[1] != path[0] - && search_ctx->ffsc_start_dir.data[1] == ':') { - search_ctx->ffsc_start_dir.data[2] = NUL; - search_ctx->ffsc_start_dir.size = 2; - } -#endif } // If stopdirs are given, split them into an array of pointers. diff --git a/src/nvim/path.c b/src/nvim/path.c index 42f4d479da..c24238a404 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -5,6 +5,9 @@ #include #include #include +#ifdef MSWIN +# include +#endif #include "auto/config.h" #include "nvim/ascii_defs.h" @@ -376,6 +379,38 @@ int path_fnamencmp(const char *const fname1, const char *const fname2, size_t le const char *p1 = fname1; const char *p2 = fname2; + +# ifdef MSWIN + // To allow proper comparisson of absolute paths: + // - one with explicit drive letter C:\xxx + // - another with implicit drive letter \xxx + // advance the pointer, of the explicit one, to skip the drive + for (int swap = 0, drive = NUL; swap < 2; swap++) { + // Handle absolute paths with implicit drive letter + c1 = utf_ptr2char(p1); + c2 = utf_ptr2char(p2); + + if ((c1 == '/' || c1 == '\\') && ASCII_ISALPHA(c2)) { + drive = mb_toupper(c2) - 'A' + 1; + + // Check for the colon + p2 += utfc_ptr2len(p2); + c2 = utf_ptr2char(p2); + if (c2 == ':' && drive == _getdrive()) { // skip the drive for comparisson + p2 += utfc_ptr2len(p2); + break; + } else { // ignore + p2 -= utfc_ptr2len(p2); + } + } + + // swap pointers + const char *tmp = p1; + p1 = p2; + p2 = tmp; + } +# endif + while (len > 0) { c1 = utf_ptr2char(p1); c2 = utf_ptr2char(p2); @@ -1834,7 +1869,7 @@ int vim_FullName(const char *fname, char *buf, size_t len, bool force) /// the root may have relative paths (like dir/../subdir) or symlinks /// embedded, or even extra separators (//). This function addresses /// those possibilities, returning a resolved absolute path. -/// For MS-Windows, this also expands names like "longna~1". +/// For MS-Windows, this also provides drive letter for all absolute paths. /// /// @param fname is the filename to expand /// @return [allocated] Full path (NULL for failure). @@ -1848,6 +1883,10 @@ char *fix_fname(const char *fname) || strstr(fname, "//") != NULL # ifdef BACKSLASH_IN_FILENAME || strstr(fname, "\\\\") != NULL +# endif +# ifdef MSWIN + || fname[0] == '/' + || fname[0] == '\\' # endif ) { return FullName_save(fname, false); @@ -2328,7 +2367,11 @@ static int path_to_absolute(const char *fname, char *buf, size_t len, int force) const char *end_of_path = fname; // expand it if forced or not an absolute path - if (force || !path_is_absolute(fname)) { + if (force || !path_is_absolute(fname) +#ifdef MSWIN // enforce drive letter on Windows paths + || fname[0] == '/' || fname[0] == '\\' +#endif + ) { p = strrchr(fname, '/'); #ifdef MSWIN if (p == NULL) { @@ -2372,8 +2415,9 @@ bool path_is_absolute(const char *fname) return false; } // A name like "d:/foo" and "//server/share" is absolute - return ((isalpha((uint8_t)fname[0]) && fname[1] == ':' && vim_ispathsep_nocolon(fname[2])) - || (vim_ispathsep_nocolon(fname[0]) && fname[0] == fname[1])); + // /foo and \foo are absolute too because windows keeps a current drive. + return ((ASCII_ISALPHA(fname[0]) && fname[1] == ':' && vim_ispathsep_nocolon(fname[2])) + || vim_ispathsep_nocolon(fname[0])); #else // UNIX: This just checks if the file name starts with '/' or '~'. return *fname == '/' || *fname == '~'; diff --git a/test/old/testdir/test_cd.vim b/test/old/testdir/test_cd.vim index 25e95ac4f6..015f7e4178 100644 --- a/test/old/testdir/test_cd.vim +++ b/test/old/testdir/test_cd.vim @@ -224,6 +224,39 @@ func Test_cd_completion() call assert_equal('"' .. cmd .. ' XComplDir1/ XComplDir2/ XComplDir3/', @:) endfor set cdpath& + + if has('win32') + " Test windows absolute path completion + " Retrieve a suitable dir in the current drive + let dir = readdir('/', 'isdirectory("/" .. v:val) && len(v:val) > 2')[-1] + " Get partial path + let partial = dir[0:-2] + " Get the current drive letter + let old = chdir('/' . dir) + let full = getcwd() + let drive = full[0] + call chdir(old) + + for cmd in ['cd', 'chdir', 'lcd', 'lchdir', 'tcd', 'tchdir'] + for sep in [ '/', '\'] + + " Explicit drive letter + call feedkeys(':' .. cmd .. ' ' .. drive .. ':' .. sep .. + \ partial .. "\\\"\", 'tx') + call assert_match(full, @:) + + " Implicit drive letter + call feedkeys(':' .. cmd .. ' ' .. sep .. partial .. "\\\"\", 'tx') + call assert_match('/' .. dir .. '/', @:) + + " UNC path + call feedkeys(':' .. cmd .. ' ' .. sep .. sep .. $COMPUTERNAME .. sep .. + \ drive .. '$' .. sep .. partial .."\\\"\", 'tx') + call assert_match('//' .. $COMPUTERNAME .. '/' .. drive .. '$/' .. dir .. '/' , @:) + + endfor + endfor + endif endfunc func Test_cd_unknown_dir() diff --git a/test/old/testdir/test_functions.vim b/test/old/testdir/test_functions.vim index d90f2f6102..b67c38349b 100644 --- a/test/old/testdir/test_functions.vim +++ b/test/old/testdir/test_functions.vim @@ -3735,7 +3735,8 @@ func Test_isabsolutepath() call assert_true(isabsolutepath('A:\Foo')) call assert_true(isabsolutepath('A:/Foo')) call assert_false(isabsolutepath('A:Foo')) - call assert_false(isabsolutepath('\Windows')) + call assert_true(isabsolutepath('\Windows')) + call assert_true(isabsolutepath('/Windows')) call assert_true(isabsolutepath('\\Server2\Share\Test\Foo.txt')) else call assert_true(isabsolutepath('/')) From 3e57c5216fadc3dfe5ab1d04388e2b4de1b0e8c6 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sun, 4 Jan 2026 18:48:34 +0800 Subject: [PATCH 2/3] vim-patch:9.1.1648: MS-Windows: some style issues with patch v9.1.1646 Problem: MS-Windows: some style issues with patch v9.1.1646 Solution: Fix typose and code style issues (zeertzjq). closes: vim/vim#18036 https://github.com/vim/vim/commit/14afa278c1bcb2df51b647ffda2821c5294eb7ff --- src/nvim/eval/fs.c | 2 +- src/nvim/path.c | 6 +++--- test/old/testdir/test_cd.vim | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/nvim/eval/fs.c b/src/nvim/eval/fs.c index ba6620d82b..65e27b0620 100644 --- a/src/nvim/eval/fs.c +++ b/src/nvim/eval/fs.c @@ -112,7 +112,7 @@ repeat: // FullName_save() is slow, don't use it when not needed. if (*p != NUL || !vim_isAbsName(*fnamep) -#ifdef MSWIN // enforce drive letter on windows paths +#ifdef MSWIN // enforce drive letter on Windows paths || **fnamep == '/' || **fnamep == '\\' #endif ) { diff --git a/src/nvim/path.c b/src/nvim/path.c index c24238a404..9d4c8d7bde 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -381,7 +381,7 @@ int path_fnamencmp(const char *const fname1, const char *const fname2, size_t le const char *p2 = fname2; # ifdef MSWIN - // To allow proper comparisson of absolute paths: + // To allow proper comparison of absolute paths: // - one with explicit drive letter C:\xxx // - another with implicit drive letter \xxx // advance the pointer, of the explicit one, to skip the drive @@ -396,7 +396,7 @@ int path_fnamencmp(const char *const fname1, const char *const fname2, size_t le // Check for the colon p2 += utfc_ptr2len(p2); c2 = utf_ptr2char(p2); - if (c2 == ':' && drive == _getdrive()) { // skip the drive for comparisson + if (c2 == ':' && drive == _getdrive()) { // skip the drive for comparison p2 += utfc_ptr2len(p2); break; } else { // ignore @@ -2415,7 +2415,7 @@ bool path_is_absolute(const char *fname) return false; } // A name like "d:/foo" and "//server/share" is absolute - // /foo and \foo are absolute too because windows keeps a current drive. + // /foo and \foo are absolute too because Windows keeps a current drive. return ((ASCII_ISALPHA(fname[0]) && fname[1] == ':' && vim_ispathsep_nocolon(fname[2])) || vim_ispathsep_nocolon(fname[0])); #else diff --git a/test/old/testdir/test_cd.vim b/test/old/testdir/test_cd.vim index 015f7e4178..5d6d4018c4 100644 --- a/test/old/testdir/test_cd.vim +++ b/test/old/testdir/test_cd.vim @@ -226,7 +226,7 @@ func Test_cd_completion() set cdpath& if has('win32') - " Test windows absolute path completion + " Test Windows absolute path completion " Retrieve a suitable dir in the current drive let dir = readdir('/', 'isdirectory("/" .. v:val) && len(v:val) > 2')[-1] " Get partial path From 6c3502d85ab4d8fa26378b532290a2bac06d4fd2 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sun, 4 Jan 2026 18:50:50 +0800 Subject: [PATCH 3/3] vim-patch:9.1.2050: tests: Test_cd_completion may fail Problem: tests: Test_cd_completion() may fail depending on the contents of the root directory of the current drive on Windows. readdir() may return a directory that cannot "cd" to, causing this test to fail. An example of such a directory is "System Volume Information" which only admin can "cd" to. Solution: When determining the directory to use for testing, use the directory that we actually "cd" to successfully. In addition, directories with '$' in their names are also excluded, as they are considered environment variables during completion and do not work as expected. Example: "$RECYCLE.BIN" (Muraoka Taro). closes: vim/vim#19078 https://github.com/vim/vim/commit/6625ba359e33f8223d146f3ae87a880c446b5470 Co-authored-by: Muraoka Taro --- test/old/testdir/test_cd.vim | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/test/old/testdir/test_cd.vim b/test/old/testdir/test_cd.vim index 5d6d4018c4..1b0ef9ef81 100644 --- a/test/old/testdir/test_cd.vim +++ b/test/old/testdir/test_cd.vim @@ -227,15 +227,42 @@ func Test_cd_completion() if has('win32') " Test Windows absolute path completion + let saved_cwd = getcwd() + " Retrieve a suitable dir in the current drive - let dir = readdir('/', 'isdirectory("/" .. v:val) && len(v:val) > 2')[-1] + for d in readdir('/', 'isdirectory("/" .. v:val) && len(v:val) > 2') + " Paths containing '$' such as "$RECYCLE.BIN" are skipped because + " they are considered environment variables and completion does not + " work. + if d =~ '\V$' + continue + endif + " Skip directories that we don't have permission to "cd" into by + " actually "cd"ing into them and making sure they don't fail. + " Directory "System Volume Information" is an example of this. + try + call chdir('/' .. d) + let dir = d + " Yay! We found a suitable dir! + break + catch /:E472:/ + " Just skip directories where "cd" fails + continue + finally + call chdir(saved_cwd) + endtry + endfor + if !exists('dir') + throw 'Skipped: no testable directories found in the current drive root' + endif + " Get partial path let partial = dir[0:-2] - " Get the current drive letter - let old = chdir('/' . dir) + " Get the current drive letter and full path of the target dir + call chdir('/' .. dir) let full = getcwd() let drive = full[0] - call chdir(old) + call chdir(saved_cwd) for cmd in ['cd', 'chdir', 'lcd', 'lchdir', 'tcd', 'tchdir'] for sep in [ '/', '\']