Skip to content

Commit 29d0f7a

Browse files
authored
checker,parser: add multifile gotodef support for -line-info (#26167)
1 parent 4391659 commit 29d0f7a

7 files changed

Lines changed: 181 additions & 5 deletions

File tree

‎vlib/v/checker/autocomplete.v‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,10 @@ fn (c &Checker) vls_is_the_node(pos token.Pos) bool {
507507
if pos.file_idx < 0 {
508508
return false
509509
}
510-
if c.pref.linfo.path != c.table.filelist[pos.file_idx] {
510+
// Normalize paths for comparison to handle directory compilation
511+
linfo_path := os.real_path(c.pref.linfo.path)
512+
file_path := os.real_path(c.table.filelist[pos.file_idx])
513+
if linfo_path != file_path {
511514
return false
512515
}
513516
if c.pref.linfo.col > pos.col + pos.len || c.pref.linfo.col < pos.col {

‎vlib/v/checker/checker.v‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,14 +387,27 @@ pub fn (mut c Checker) check_files(ast_files []&ast.File) {
387387
mut has_main_mod_file := false
388388
mut has_no_main_mod_file := false
389389
mut has_main_fn := false
390+
// Determine the project directory when using -line-info
391+
mut project_dir := ''
392+
if c.pref.is_vls && c.pref.line_info != '' {
393+
project_dir = if os.is_dir(c.pref.path) {
394+
os.real_path(c.pref.path)
395+
} else {
396+
os.real_path(os.dir(c.pref.linfo.path))
397+
}
398+
}
390399
unsafe {
391400
mut files_from_main_module := []&ast.File{}
392401
for i in 0 .. ast_files.len {
393402
mut file := ast_files[i]
394-
if c.pref.is_vls && file.path != c.pref.path {
395-
// in `vls` mode, only check the user file
403+
if c.pref.is_vls && c.pref.line_info == '' && file.path != c.pref.path {
396404
continue
397405
}
406+
if c.pref.is_vls && c.pref.line_info != '' && project_dir != '' {
407+
if !os.real_path(file.path).starts_with(project_dir) {
408+
continue
409+
}
410+
}
398411
c.timers.start('checker_check ${file.path}')
399412
c.check(mut file)
400413
if file.mod.name == 'no_main' {

‎vlib/v/parser/parser.v‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,21 @@ pub fn (mut p Parser) set_path(path string) {
266266
}
267267
}
268268

269+
fn should_skip_vls_file(pref_ &pref.Preferences, path string) bool {
270+
if !pref_.is_vls {
271+
return false
272+
}
273+
if pref_.line_info != '' {
274+
project_dir := if os.is_dir(pref_.path) {
275+
os.real_path(pref_.path)
276+
} else {
277+
os.real_path(os.dir(pref_.linfo.path))
278+
}
279+
return !os.real_path(path).starts_with(project_dir)
280+
}
281+
return path != pref_.path
282+
}
283+
269284
pub fn parse_file(path string, mut table ast.Table, comments_mode scanner.CommentsMode, pref_ &pref.Preferences) &ast.File {
270285
// Note: when comments_mode == .toplevel_comments,
271286
// the parser gives feedback to the scanner about toplevel statements, so that the scanner can skip
@@ -287,7 +302,7 @@ pub fn parse_file(path string, mut table ast.Table, comments_mode scanner.Commen
287302
// Only set vls mode if it's the file the user requested via `v -vls-mode file.v`
288303
// Otherwise we'd be parsing entire stdlib in vls mode
289304
is_vls: pref_.is_vls && path == pref_.path
290-
is_vls_skip_file: pref_.is_vls && path != pref_.path
305+
is_vls_skip_file: should_skip_vls_file(pref_, path)
291306
scope: &ast.Scope{
292307
start_pos: 0
293308
parent: table.global_scope

‎vlib/v/tests/vls/goto_def_test.v‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import os
22
import term
33
import v.util.diff
44

5-
const vroot = os.real_path(@VMODROOT)
5+
const vroot = @VMODROOT
66
const test_file = os.join_path(vroot, 'vlib', 'v', 'tests', 'vls', 'goto_def_test_data.vv')
77
const mod1_text_file = os.join_path(vroot, 'vlib', 'v', 'tests', 'vls', 'sample_mod1',
88
'sample.v')
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module main
2+
3+
fn main() {
4+
obj := MyStruct{
5+
value: 42
6+
name: 'test'
7+
}
8+
val := obj.get_value()
9+
println(val)
10+
11+
field_val := obj.value
12+
println(field_val)
13+
14+
e := MyEnum.first
15+
println(e)
16+
17+
match e {
18+
.second {
19+
println('second')
20+
}
21+
else {}
22+
}
23+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module main
2+
3+
pub struct MyStruct {
4+
value int
5+
name string
6+
}
7+
8+
pub enum MyEnum {
9+
first
10+
second
11+
third
12+
}
13+
14+
pub fn (ms MyStruct) get_value() int {
15+
return ms.value
16+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import os
2+
import term
3+
4+
const vroot = @VMODROOT
5+
const test_dir = os.join_path(vroot, 'vlib', 'v', 'tests', 'vls', 'multifile_gotodef')
6+
const main_file = os.join_path(test_dir, 'main.v')
7+
const types_file = os.join_path(test_dir, 'types.v')
8+
const types_expected = os.join_path('multifile_gotodef', 'types.v')
9+
10+
struct TestCase {
11+
name string
12+
line int
13+
col int
14+
expected string
15+
description string
16+
}
17+
18+
const test_cases = [
19+
TestCase{
20+
name: 'struct_name_cross_file'
21+
line: 4
22+
col: 12
23+
expected: types_expected + ':3:11'
24+
description: 'Go to struct definition in another file (MyStruct in types.v)'
25+
},
26+
TestCase{
27+
name: 'method_call_cross_file'
28+
line: 8
29+
col: 18
30+
expected: types_expected + ':14:21'
31+
description: 'Go to method definition in another file (get_value in types.v)'
32+
},
33+
TestCase{
34+
name: 'struct_field_cross_file'
35+
line: 11
36+
col: 22
37+
expected: types_expected + ':4:1'
38+
description: 'Go to struct field definition in another file (value in types.v)'
39+
},
40+
TestCase{
41+
name: 'enum_value_cross_file'
42+
line: 14
43+
col: 15
44+
expected: types_expected + ':9:1'
45+
description: 'Go to enum value definition in another file (first in types.v)'
46+
},
47+
TestCase{
48+
name: 'enum_short_form_cross_file'
49+
line: 18
50+
col: 4
51+
expected: types_expected + ':10:1'
52+
description: 'Go to enum value definition from short form in match (.second in types.v)'
53+
},
54+
]
55+
56+
fn test_multifile_goto_definition() {
57+
mut total_errors := 0
58+
mut passed := 0
59+
60+
// Change to vls directory so relative paths match expected output
61+
original_dir := os.getwd()
62+
vls_dir := os.join_path(vroot, 'vlib', 'v', 'tests', 'vls')
63+
os.chdir(vls_dir) or { panic(err) }
64+
defer {
65+
os.chdir(original_dir) or {}
66+
}
67+
68+
for tc in test_cases {
69+
cmd := 'v -w -check -json-errors -nocolor -vls-mode -line-info "${main_file}:${tc.line}:gd^${tc.col}" ${os.quoted_path('multifile_gotodef')}'
70+
res := os.execute(cmd)
71+
72+
if res.exit_code < 0 {
73+
println('${term.red('FAIL')} ${tc.name}: Command failed to execute')
74+
println(' Command: ${cmd}')
75+
total_errors++
76+
continue
77+
}
78+
79+
res_output := $if windows {
80+
res.output.replace('\r\n', '\n').trim_space()
81+
} $else {
82+
res.output.trim_space()
83+
}
84+
85+
if tc.expected != res_output {
86+
println('${term.red('FAIL')} ${tc.name}')
87+
println(' Description: ${tc.description}')
88+
println(' Line ${tc.line}, Column ${tc.col}')
89+
println(' Expected: ${tc.expected}')
90+
println(' Got: ${res_output}')
91+
total_errors++
92+
} else {
93+
println('${term.green('OK ')} ${tc.name}: ${tc.description}')
94+
passed++
95+
}
96+
}
97+
98+
println('')
99+
println('${term.header('Summary:', '=')}')
100+
println('Passed: ${passed}/${test_cases.len}')
101+
if total_errors > 0 {
102+
println('${term.red('Failed:')} ${total_errors}')
103+
}
104+
105+
assert total_errors == 0, 'Some tests failed'
106+
}

0 commit comments

Comments
 (0)