From 29d0f7aa8f9f78fa28bdff576394cd05842d5e36 Mon Sep 17 00:00:00 2001 From: CreeperFace <165158232+dy-tea@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:39:34 +0000 Subject: [PATCH] checker,parser: add multifile gotodef support for -line-info (#26167) --- vlib/v/checker/autocomplete.v | 5 +- vlib/v/checker/checker.v | 17 +++- vlib/v/parser/parser.v | 17 +++- vlib/v/tests/vls/goto_def_test.v | 2 +- vlib/v/tests/vls/multifile_gotodef/main.v | 23 +++++ vlib/v/tests/vls/multifile_gotodef/types.v | 16 ++++ vlib/v/tests/vls/multifile_gotodef_test.v | 106 +++++++++++++++++++++ 7 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 vlib/v/tests/vls/multifile_gotodef/main.v create mode 100644 vlib/v/tests/vls/multifile_gotodef/types.v create mode 100644 vlib/v/tests/vls/multifile_gotodef_test.v diff --git a/vlib/v/checker/autocomplete.v b/vlib/v/checker/autocomplete.v index 336af8848..f8cb1102f 100644 --- a/vlib/v/checker/autocomplete.v +++ b/vlib/v/checker/autocomplete.v @@ -507,7 +507,10 @@ fn (c &Checker) vls_is_the_node(pos token.Pos) bool { if pos.file_idx < 0 { return false } - if c.pref.linfo.path != c.table.filelist[pos.file_idx] { + // Normalize paths for comparison to handle directory compilation + linfo_path := os.real_path(c.pref.linfo.path) + file_path := os.real_path(c.table.filelist[pos.file_idx]) + if linfo_path != file_path { return false } if c.pref.linfo.col > pos.col + pos.len || c.pref.linfo.col < pos.col { diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 5b5940b01..46996f805 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -387,14 +387,27 @@ pub fn (mut c Checker) check_files(ast_files []&ast.File) { mut has_main_mod_file := false mut has_no_main_mod_file := false mut has_main_fn := false + // Determine the project directory when using -line-info + mut project_dir := '' + if c.pref.is_vls && c.pref.line_info != '' { + project_dir = if os.is_dir(c.pref.path) { + os.real_path(c.pref.path) + } else { + os.real_path(os.dir(c.pref.linfo.path)) + } + } unsafe { mut files_from_main_module := []&ast.File{} for i in 0 .. ast_files.len { mut file := ast_files[i] - if c.pref.is_vls && file.path != c.pref.path { - // in `vls` mode, only check the user file + if c.pref.is_vls && c.pref.line_info == '' && file.path != c.pref.path { continue } + if c.pref.is_vls && c.pref.line_info != '' && project_dir != '' { + if !os.real_path(file.path).starts_with(project_dir) { + continue + } + } c.timers.start('checker_check ${file.path}') c.check(mut file) if file.mod.name == 'no_main' { diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 484eabdcb..e0fee6ef1 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -266,6 +266,21 @@ pub fn (mut p Parser) set_path(path string) { } } +fn should_skip_vls_file(pref_ &pref.Preferences, path string) bool { + if !pref_.is_vls { + return false + } + if pref_.line_info != '' { + project_dir := if os.is_dir(pref_.path) { + os.real_path(pref_.path) + } else { + os.real_path(os.dir(pref_.linfo.path)) + } + return !os.real_path(path).starts_with(project_dir) + } + return path != pref_.path +} + pub fn parse_file(path string, mut table ast.Table, comments_mode scanner.CommentsMode, pref_ &pref.Preferences) &ast.File { // Note: when comments_mode == .toplevel_comments, // 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 // Only set vls mode if it's the file the user requested via `v -vls-mode file.v` // Otherwise we'd be parsing entire stdlib in vls mode is_vls: pref_.is_vls && path == pref_.path - is_vls_skip_file: pref_.is_vls && path != pref_.path + is_vls_skip_file: should_skip_vls_file(pref_, path) scope: &ast.Scope{ start_pos: 0 parent: table.global_scope diff --git a/vlib/v/tests/vls/goto_def_test.v b/vlib/v/tests/vls/goto_def_test.v index a9675c098..46329cfc3 100644 --- a/vlib/v/tests/vls/goto_def_test.v +++ b/vlib/v/tests/vls/goto_def_test.v @@ -2,7 +2,7 @@ import os import term import v.util.diff -const vroot = os.real_path(@VMODROOT) +const vroot = @VMODROOT const test_file = os.join_path(vroot, 'vlib', 'v', 'tests', 'vls', 'goto_def_test_data.vv') const mod1_text_file = os.join_path(vroot, 'vlib', 'v', 'tests', 'vls', 'sample_mod1', 'sample.v') diff --git a/vlib/v/tests/vls/multifile_gotodef/main.v b/vlib/v/tests/vls/multifile_gotodef/main.v new file mode 100644 index 000000000..a45175c9a --- /dev/null +++ b/vlib/v/tests/vls/multifile_gotodef/main.v @@ -0,0 +1,23 @@ +module main + +fn main() { + obj := MyStruct{ + value: 42 + name: 'test' + } + val := obj.get_value() + println(val) + + field_val := obj.value + println(field_val) + + e := MyEnum.first + println(e) + + match e { + .second { + println('second') + } + else {} + } +} diff --git a/vlib/v/tests/vls/multifile_gotodef/types.v b/vlib/v/tests/vls/multifile_gotodef/types.v new file mode 100644 index 000000000..ebb9ec85e --- /dev/null +++ b/vlib/v/tests/vls/multifile_gotodef/types.v @@ -0,0 +1,16 @@ +module main + +pub struct MyStruct { + value int + name string +} + +pub enum MyEnum { + first + second + third +} + +pub fn (ms MyStruct) get_value() int { + return ms.value +} diff --git a/vlib/v/tests/vls/multifile_gotodef_test.v b/vlib/v/tests/vls/multifile_gotodef_test.v new file mode 100644 index 000000000..8f086e878 --- /dev/null +++ b/vlib/v/tests/vls/multifile_gotodef_test.v @@ -0,0 +1,106 @@ +import os +import term + +const vroot = @VMODROOT +const test_dir = os.join_path(vroot, 'vlib', 'v', 'tests', 'vls', 'multifile_gotodef') +const main_file = os.join_path(test_dir, 'main.v') +const types_file = os.join_path(test_dir, 'types.v') +const types_expected = os.join_path('multifile_gotodef', 'types.v') + +struct TestCase { + name string + line int + col int + expected string + description string +} + +const test_cases = [ + TestCase{ + name: 'struct_name_cross_file' + line: 4 + col: 12 + expected: types_expected + ':3:11' + description: 'Go to struct definition in another file (MyStruct in types.v)' + }, + TestCase{ + name: 'method_call_cross_file' + line: 8 + col: 18 + expected: types_expected + ':14:21' + description: 'Go to method definition in another file (get_value in types.v)' + }, + TestCase{ + name: 'struct_field_cross_file' + line: 11 + col: 22 + expected: types_expected + ':4:1' + description: 'Go to struct field definition in another file (value in types.v)' + }, + TestCase{ + name: 'enum_value_cross_file' + line: 14 + col: 15 + expected: types_expected + ':9:1' + description: 'Go to enum value definition in another file (first in types.v)' + }, + TestCase{ + name: 'enum_short_form_cross_file' + line: 18 + col: 4 + expected: types_expected + ':10:1' + description: 'Go to enum value definition from short form in match (.second in types.v)' + }, +] + +fn test_multifile_goto_definition() { + mut total_errors := 0 + mut passed := 0 + + // Change to vls directory so relative paths match expected output + original_dir := os.getwd() + vls_dir := os.join_path(vroot, 'vlib', 'v', 'tests', 'vls') + os.chdir(vls_dir) or { panic(err) } + defer { + os.chdir(original_dir) or {} + } + + for tc in test_cases { + cmd := 'v -w -check -json-errors -nocolor -vls-mode -line-info "${main_file}:${tc.line}:gd^${tc.col}" ${os.quoted_path('multifile_gotodef')}' + res := os.execute(cmd) + + if res.exit_code < 0 { + println('${term.red('FAIL')} ${tc.name}: Command failed to execute') + println(' Command: ${cmd}') + total_errors++ + continue + } + + res_output := $if windows { + res.output.replace('\r\n', '\n').trim_space() + } $else { + res.output.trim_space() + } + + if tc.expected != res_output { + println('${term.red('FAIL')} ${tc.name}') + println(' Description: ${tc.description}') + println(' Line ${tc.line}, Column ${tc.col}') + println(' Expected: ${tc.expected}') + println(' Got: ${res_output}') + total_errors++ + } else { + println('${term.green('OK ')} ${tc.name}: ${tc.description}') + passed++ + } + } + + println('') + println('${term.header('Summary:', '=')}') + println('Passed: ${passed}/${test_cases.len}') + if total_errors > 0 { + println('${term.red('Failed:')} ${total_errors}') + } + + assert total_errors == 0, 'Some tests failed' +} -- 2.39.5