commit 43bf045005b0fe819e85fdc44d7f7563f8ccda9d
parent d548b52b0f6924eee69ab86590731d30dfd36f7d
Author: zeertzjq <zeertzjq@outlook.com>
Date: Mon, 18 Aug 2025 10:33:27 +0800
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 <miguel.barro@live.com>
Diffstat:
6 files changed, 90 insertions(+), 17 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -5,6 +5,9 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
+#ifdef MSWIN
+# include <direct.h>
+#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).
@@ -1849,6 +1884,10 @@ char *fix_fname(const char *fname)
# 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
@@ -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 .. "\<C-A>\<C-B>\"\<CR>", 'tx')
+ call assert_match(full, @:)
+
+ " Implicit drive letter
+ call feedkeys(':' .. cmd .. ' ' .. sep .. partial .. "\<C-A>\<C-B>\"\<CR>", 'tx')
+ call assert_match('/' .. dir .. '/', @:)
+
+ " UNC path
+ call feedkeys(':' .. cmd .. ' ' .. sep .. sep .. $COMPUTERNAME .. sep ..
+ \ drive .. '$' .. sep .. partial .."\<C-A>\<C-B>\"\<CR>", '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
@@ -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('/'))