fix: gf fails on "foo/bar.txt:1:2" on Windows

Problem:
On Windows, "gf" fails on a filepath that has a line:column suffix.
Example:

    E447: Can't find file "src/app/core/services/identity/identity.service.ts:64:23"

Solution:
- Remove ":" from 'isfname' on Windows. Colon is not a valid filename
  character (except for the drive-letter).
- Handle drive letters specially in file_name_in_line().

Fixes #25160
This commit is contained in:
Leonardo Mello 2023-09-18 16:50:47 -03:00 committed by Justin M. Keyes
parent 4e4ad4312e
commit 1dd700a8d9
7 changed files with 92 additions and 8 deletions

View File

@ -15,6 +15,9 @@ BREAKING CHANGES *news-breaking*
The following changes may require adaptations in user config or plugins. The following changes may require adaptations in user config or plugins.
• Windows file path drive letters are now detected even if ":" is not in
'isfname'. The default 'isfname' no longer includes ":".
• |vim.tbl_islist()| now checks whether a table is actually list-like (i.e., • |vim.tbl_islist()| now checks whether a table is actually list-like (i.e.,
has integer keys without gaps and starting from 1). For the previous has integer keys without gaps and starting from 1). For the previous
behavior (only check for integer keys, allow gaps or not starting with 1), behavior (only check for integer keys, allow gaps or not starting with 1),

View File

@ -3453,7 +3453,7 @@ A jump table for the options with a short description can be found at |Q_op|.
*'isfname'* *'isf'* *'isfname'* *'isf'*
'isfname' 'isf' string (default for Windows: 'isfname' 'isf' string (default for Windows:
"@,48-57,/,\,.,-,_,+,,,#,$,%,{,},[,],:,@-@,!,~,=" "@,48-57,/,\,.,-,_,+,,,#,$,%,{,},[,],@-@,!,~,="
otherwise: "@,48-57,/,.,-,_,+,,,#,$,%,~,=") otherwise: "@,48-57,/,.,-,_,+,,,#,$,%,~,=")
global global
The characters specified by this option are included in file names and The characters specified by this option are included in file names and

View File

@ -53,6 +53,8 @@ Defaults *nvim-defaults*
- 'hlsearch' is enabled - 'hlsearch' is enabled
- 'include' defaults to "". The C ftplugin sets it to "^\\s*#\\s*include" - 'include' defaults to "". The C ftplugin sets it to "^\\s*#\\s*include"
- 'incsearch' is enabled - 'incsearch' is enabled
- 'isfname' does not include ":" on Windows. Include ":" in 'isfname' to treat
it as part of a filename anywhere in the name (not only the drive letter).
- 'joinspaces' is disabled - 'joinspaces' is disabled
- 'langnoremap' is enabled - 'langnoremap' is enabled
- 'langremap' is disabled - 'langremap' is disabled

View File

@ -4298,9 +4298,9 @@ return {
defaults = { defaults = {
condition = 'BACKSLASH_IN_FILENAME', condition = 'BACKSLASH_IN_FILENAME',
if_false = '@,48-57,/,.,-,_,+,,,#,$,%,~,=', if_false = '@,48-57,/,.,-,_,+,,,#,$,%,~,=',
if_true = '@,48-57,/,\\,.,-,_,+,,,#,$,%,{,},[,],:,@-@,!,~,=', if_true = '@,48-57,/,\\,.,-,_,+,,,#,$,%,{,},[,],@-@,!,~,=',
doc = [[for Windows: doc = [[for Windows:
"@,48-57,/,\,.,-,_,+,,,#,$,%,{,},[,],:,@-@,!,~,=" "@,48-57,/,\,.,-,_,+,,,#,$,%,{,},[,],@-@,!,~,="
otherwise: "@,48-57,/,.,-,_,+,,,#,$,%,~,="]], otherwise: "@,48-57,/,.,-,_,+,,,#,$,%,~,="]],
}, },
deny_duplicates = true, deny_duplicates = true,

View File

@ -6943,11 +6943,12 @@ char *file_name_in_line(char *line, int col, int options, int count, char *rel_f
bool is_url = false; bool is_url = false;
// Search backward for first char of the file name. // Search backward for first char of the file name.
// Go one char back to ":" before "//" even when ':' is not in 'isfname'. // Go one char back to ":" before "//", or to the drive letter before ":\" (even if ":"
// is not in 'isfname').
while (ptr > line) { while (ptr > line) {
if ((len = (size_t)(utf_head_off(line, ptr - 1))) > 0) { if ((len = (size_t)(utf_head_off(line, ptr - 1))) > 0) {
ptr -= len + 1; ptr -= len + 1;
} else if (vim_isfilec((uint8_t)ptr[-1]) } else if (vim_isfilec((uint8_t)ptr[-1]) || path_has_drive_letter(ptr - 2)
|| ((options & FNAME_HYP) && path_is_url(ptr - 1))) { || ((options & FNAME_HYP) && path_is_url(ptr - 1))) {
ptr--; ptr--;
} else { } else {
@ -6957,14 +6958,13 @@ char *file_name_in_line(char *line, int col, int options, int count, char *rel_f
// Search forward for the last char of the file name. // Search forward for the last char of the file name.
// Also allow ":/" when ':' is not in 'isfname'. // Also allow ":/" when ':' is not in 'isfname'.
len = 0; len = path_has_drive_letter(ptr) ? 2 : 0;
while (vim_isfilec((uint8_t)ptr[len]) || (ptr[len] == '\\' && ptr[len + 1] == ' ') while (vim_isfilec((uint8_t)ptr[len]) || (ptr[len] == '\\' && ptr[len + 1] == ' ')
|| ((options & FNAME_HYP) && path_is_url(ptr + len)) || ((options & FNAME_HYP) && path_is_url(ptr + len))
|| (is_url && vim_strchr(":?&=", (uint8_t)ptr[len]) != NULL)) { || (is_url && vim_strchr(":?&=", (uint8_t)ptr[len]) != NULL)) {
// After type:// we also include :, ?, & and = as valid characters, so that // After type:// we also include :, ?, & and = as valid characters, so that
// http://google.com:8080?q=this&that=ok works. // http://google.com:8080?q=this&that=ok works.
if ((ptr[len] >= 'A' && ptr[len] <= 'Z') if ((ptr[len] >= 'A' && ptr[len] <= 'Z') || (ptr[len] >= 'a' && ptr[len] <= 'z')) {
|| (ptr[len] >= 'a' && ptr[len] <= 'z')) {
if (in_type && path_is_url(ptr + len + 1)) { if (in_type && path_is_url(ptr + len + 1)) {
is_url = true; is_url = true;
} }

View File

@ -91,4 +91,80 @@ describe('file search', function()
feed('gf') feed('gf')
eq('filename_with_unicode_ααα', eval('expand("%:t")')) eq('filename_with_unicode_ααα', eval('expand("%:t")'))
end) end)
it('matches Windows drive-letter filepaths (without ":" in &isfname)', function()
local os_win = is_os('win')
insert([[c:/d:/foo/bar.txt]])
eq([[c:/d:/foo/bar.txt]], eval('expand("<cfile>")'))
command('%delete')
insert([[//share/c:/foo/bar/]])
eq([[//share/c:/foo/bar/]], eval('expand("<cfile>")'))
command('%delete')
insert([[file://c:/foo/bar]])
eq([[file://c:/foo/bar]], eval('expand("<cfile>")'))
command('%delete')
insert([[https://c:/foo/bar]])
eq([[https://c:/foo/bar]], eval('expand("<cfile>")'))
command('%delete')
insert([[\foo\bar]])
eq(os_win and [[\foo\bar]] or [[bar]], eval('expand("<cfile>")'))
command('%delete')
insert([[/foo/bar]])
eq([[/foo/bar]], eval('expand("<cfile>")'))
command('%delete')
insert([[c:\foo\bar]])
eq(os_win and [[c:\foo\bar]] or [[bar]], eval('expand("<cfile>")'))
command('%delete')
insert([[c:/foo/bar]])
eq([[c:/foo/bar]], eval('expand("<cfile>")'))
command('%delete')
insert([[c:foo\bar]])
eq(os_win and [[foo\bar]] or [[bar]], eval('expand("<cfile>")'))
command('%delete')
insert([[c:foo/bar]])
eq([[foo/bar]], eval('expand("<cfile>")'))
command('%delete')
insert([[c:foo]])
eq([[foo]], eval('expand("<cfile>")'))
command('%delete')
-- Examples from: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#example-ways-to-refer-to-the-same-file
insert([[c:\temp\test-file.txt]])
eq(os_win and [[c:\temp\test-file.txt]] or [[test-file.txt]], eval('expand("<cfile>")'))
command('%delete')
insert([[\\127.0.0.1\c$\temp\test-file.txt]])
eq(os_win and [[\\127.0.0.1\c$\temp\test-file.txt]] or [[test-file.txt]], eval('expand("<cfile>")'))
command('%delete')
insert([[\\LOCALHOST\c$\temp\test-file.txt]])
eq(os_win and [[\\LOCALHOST\c$\temp\test-file.txt]] or [[test-file.txt]], eval('expand("<cfile>")'))
command('%delete')
insert([[\\.\c:\temp\test-file.txt]]) -- not supported yet
eq(os_win and [[\\.\c]] or [[test-file.txt]], eval('expand("<cfile>")'))
command('%delete')
insert([[\\?\c:\temp\test-file.txt]]) -- not supported yet
eq(os_win and [[\c]] or [[test-file.txt]], eval('expand("<cfile>")'))
command('%delete')
insert([[\\.\UNC\LOCALHOST\c$\temp\test-file.txt]])
eq(os_win and [[\\.\UNC\LOCALHOST\c$\temp\test-file.txt]] or [[test-file.txt]], eval('expand("<cfile>")'))
command('%delete')
insert([[\\127.0.0.1\c$\temp\test-file.txt]])
eq(os_win and [[\\127.0.0.1\c$\temp\test-file.txt]] or [[test-file.txt]], eval('expand("<cfile>")'))
end)
end) end)

View File

@ -26,6 +26,9 @@ if exists('s:did_load')
set sessionoptions+=options set sessionoptions+=options
set viewoptions+=options set viewoptions+=options
set switchbuf= set switchbuf=
if has('win32')
set isfname+=:
endif
if g:testname !~ 'test_mapping.vim$' if g:testname !~ 'test_mapping.vim$'
" Make "Q" switch to Ex mode. " Make "Q" switch to Ex mode.
" This does not work for all tests. " This does not work for all tests.