From 289eb5ad82ef861bc6f1e4e2afaa8243f52baec3 Mon Sep 17 00:00:00 2001 From: metif12 Date: Sat, 3 Oct 2026 15:06:39 +0330 Subject: [PATCH 1/7] fix: recognize a launcher that cannot reach the V1 compatibility compiler Hover, completion, signature help, and go to definition all go through `v -vls-mode -line-info`, which the V launcher routes to the V1 compatibility compiler. When that compiler is missing, the launcher refuses with a single line and exits: `-vls-mode` requires the compatibility compiler, but no usable V 0.5.2 fallback was found and make is unavailable. Install make, then run `make v1` in `C:\Users\me\v`. That refusal is not an "unknown option" line, so neither `compiler_rejects_line_info` nor `compiler_refused_and_stopped` recognized it. `line_info_mode` stayed `.direct`, so VLS kept spawning the compiler once per request for an answer that can never arrive, and every one of those lookups resolved to empty with nothing said about why. Recognize the refusal, retire the lookups as `.missing` so they are answered from VLS's own index instead of paying a process launch each, and tell the user what to install. A launcher that can build the fallback itself announces "running `make v1` now" and then answers, so that form is explicitly not a dead end. The notice needs no "already warned" flag: the caller sets `line_info_mode` to `.missing` first, and from then on `run_v_line_info` returns from its early `.missing` check without reaching this point, so it is sent at most once per session. --- interop.v | 104 ++++++++++++++----- interop_test.v | 270 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 258 insertions(+), 116 deletions(-) diff --git a/interop.v b/interop.v index 0aad112f..5720d7b5 100644 --- a/interop.v +++ b/interop.v @@ -267,7 +267,8 @@ fn build_v_check_args_multifile(is_library_module bool) []string { } fn build_v_line_info_args_multifile(rel_file string, line_info string) []string { - return ['-w', '-check', '-nocolor', '-vls-mode', '-line-info', '${rel_file}:${line_info}', '.'] + return ['-w', '-check', '-nocolor', '-vls-mode', '-line-info', '${rel_file}:${line_info}', + '.'] } fn build_v_line_info_args_single(file_to_check string, line_info string, compile_target string) []string { @@ -284,8 +285,8 @@ fn build_v_line_info_args_single(file_to_check string, line_info string, compile // than assumed from a version number. enum LineInfoMode { unknown // not probed yet — try the options as-is - direct // the compiler answers `-vls-mode` / `-line-info` itself - compat // reaching the checker needs `-old-compiler` + direct // the compiler answers `-vls-mode` / `-line-info` itself + compat // reaching the checker needs `-old-compiler` missing // no compatibility compiler either — answer from VLS's own index } @@ -338,6 +339,56 @@ fn compiler_refused_and_stopped(output string) bool { return refusals > 0 } +// compiler_lacks_compatibility_compiler reports whether an invocation failed +// because the V launcher could not reach the V1 compatibility compiler, which is +// what owns `-vls-mode` / `-line-info`. The launcher prints a single-line +// refusal and exits without compiling, and that refusal is not an "unknown +// option" line, so `compiler_rejects_line_info` never sees it. Without this +// check VLS keeps paying a process launch per request for an answer that can +// never arrive, and answers every hover, completion, and definition with an +// empty result while saying nothing about why. Each refusal form below is one +// `ensure_v1_fallback` failure in the launcher. +fn compiler_lacks_compatibility_compiler(output string) bool { + for line in output.split_into_lines() { + trimmed := line.trim_space() + if trimmed == '' { + continue + } + // A launcher that can build the fallback itself announces that first and + // then answers the request, so this form must retire nothing. + if trimmed.contains('running `make v1` now') { + return false + } + if trimmed.contains('requires the compatibility compiler') { + return true + } + if trimmed.contains('`-old-compiler` was requested') { + return true + } + if trimmed.starts_with('`make v1` failed') { + return true + } + if trimmed.starts_with('`make v1` completed without installing') { + return true + } + } + return false +} + +// report_missing_compatibility_compiler tells the user that the configured `v` +// cannot serve the compiler-backed lookups, and how to fix it. Without this the +// editor shows a VLS that highlights code but silently answers nothing for +// completion, hover, signature help, and go to definition, which is +// indistinguishable from VLS being broken. +// +// No "already warned" flag is needed: the caller sets `line_info_mode` to +// `.missing` first, and from then on `run_v_line_info` returns from its early +// `.missing` check without reaching this point, so it is reached at most once per +// session. +fn (mut app App) report_missing_compatibility_compiler() { + app.send_show_message('vls: the V compiler on PATH cannot serve completion, hover, signature help, or go to definition, because its V1 compatibility compiler is missing. Install `make`, then run `make v1` in your V source directory, or point `v.vls.command` at a V that has it. Diagnostics and formatting are unaffected.', 2) +} + // normalize_v_line_info_output extracts the actual line-info payload from the // compatibility compiler's combined stdout/stderr. Newer launchers may prepend // an option notice before delegating to the established compiler. @@ -465,11 +516,11 @@ fn parse_v_check_diagnostic_header(line string, source_dir string) ?JsonError { if marker_idx > best_marker_idx { best_marker_idx = marker_idx best = JsonError{ - path: path + path: path message: line[marker_idx + marker.len..] line_nr: line_nr_text.int() - col: col_text.int() - level: if level.contains('error') { 'error' } else { level } + col: col_text.int() + level: if level.contains('error') { 'error' } else { level } } } break @@ -517,8 +568,8 @@ fn (mut app App) cache_v_check_result(path string, content_hash int, generation } app.diag_cache[path] = DiagCacheEntry{ content_hash: content_hash - generation: generation - errors: errors + generation: generation + errors: errors } } @@ -553,7 +604,7 @@ fn run_v_argv(args []string, work_folder string) os.Result { log(msg) return os.Result{ exit_code: 1 - output: msg + output: msg } } v_exe := resolve_v_compiler_exe() @@ -615,12 +666,12 @@ fn run_v_argv(args []string, work_folder string) os.Result { if timed_out { return os.Result{ exit_code: compiler_exit_timeout - output: '' + output: '' } } return os.Result{ exit_code: code - output: out.str() + output: out.str() } } @@ -728,12 +779,12 @@ fn (mut app App) prepare_compilation_overlay(real_path string) !CompilationOverl } } return CompilationOverlay{ - source_root: source_root + source_root: source_root source_display_root: source_display_root - temp_root: temp_root - source_work_dir: source_work_dir - temp_work_dir: temp_work_dir - temp_source_file: os.join_path(temp_root, file_rel) + temp_root: temp_root + source_work_dir: source_work_dir + temp_work_dir: temp_work_dir + temp_source_file: os.join_path(temp_root, file_rel) } } @@ -840,12 +891,12 @@ fn (mut app App) run_v_check(path string, text string) []JsonError { err_file := source_path_from_overlay(err.path, overlay) if normalized_index_path(err_file) == normalized_index_path(real_path) { updated_err := JsonError{ - path: real_path + path: real_path message: err.message line_nr: err.line_nr - col: err.col - len: err.len - level: err.level + col: err.col + len: err.len + level: err.level } filtered_errors << updated_err log('INCLUDING ERROR from err_file=${err_file}: ${err.message}') @@ -1513,7 +1564,7 @@ fn (mut app App) line_info_unavailable_result(method Method, path string, line_i } return Hover{ contents: MarkupContent{ - kind: 'markdown' + kind: 'markdown' value: doc } } @@ -1630,7 +1681,7 @@ fn (mut app App) run_v_line_info(method Method, path string, line_info string) R output = normalize_v_line_info_output(x.output, method) } } - if compiler_rejects_line_info(x.output) && compiler_refused_and_stopped(x.output) { + if (compiler_rejects_line_info(x.output) && compiler_refused_and_stopped(x.output)) || compiler_lacks_compatibility_compiler(x.output) { // The invocation refused the options and did nothing else, so nothing // here can answer and the single-file retry below would be refused for // the same reason. An empty payload alone is not evidence: on a launcher @@ -1639,6 +1690,7 @@ fn (mut app App) run_v_line_info(method Method, path string, line_info string) R // compiler-backed hover, signature, and receiver definition. log('no compiler serves -line-info; falling back to the index') app.line_info_mode = .missing + app.report_missing_compatibility_compiler() cleanup_compilation_temp(temp_project_dir, singlefile_tmppath) return app.line_info_unavailable_result(method, path, line_info) } @@ -1696,7 +1748,7 @@ fn (mut app App) run_v_line_info(method Method, path string, line_info string) R } result = Hover{ contents: MarkupContent{ - kind: 'markdown' + kind: 'markdown' value: value } } @@ -1704,7 +1756,7 @@ fn (mut app App) run_v_line_info(method Method, path string, line_info string) R // Compiler returned no info but we found a vdoc comment result = Hover{ contents: MarkupContent{ - kind: 'markdown' + kind: 'markdown' value: doc } } @@ -1749,13 +1801,13 @@ fn (app &App) compiler_location(path string, line int, byte_col int) Location { target_uri := index_uri_for_path(path, app.open_index_uris_by_path()) client_col := app.byte_col_to_client_col(target_uri, line, byte_col) return Location{ - uri: target_uri + uri: target_uri range: LSPRange{ start: Position{ line: line char: client_col } - end: Position{ + end: Position{ line: line char: client_col } diff --git a/interop_test.v b/interop_test.v index 3f03a75a..86308622 100644 --- a/interop_test.v +++ b/interop_test.v @@ -164,8 +164,8 @@ fn test_normalize_overlay_path_preserves_posix_backslashes() { fn test_source_path_from_overlay_normalizes_windows_relative_join() { overlay := CompilationOverlay{ source_display_root: r'C:\repo' - temp_root: r'C:\temp\overlay' - temp_work_dir: r'C:\temp\overlay\src' + temp_root: r'C:\temp\overlay' + temp_work_dir: r'C:\temp\overlay\src' } mapped := source_path_from_overlay_with_windows_rules('./main.v', overlay, true) assert mapped == 'C:/repo/src/main.v' @@ -257,7 +257,7 @@ fn test_compiler_location_reuses_equivalent_open_uri() { canonical_uri := path_to_uri(path) open_uri := canonical_uri.replace_once('file:///', 'file://localhost/') mut app := &App{ - open_files: map[string]string{} + open_files: map[string]string{} position_encoding: .utf16 } app.open_files[open_uri] = '🚀 target\n' @@ -471,6 +471,96 @@ fn test_compiler_refused_and_stopped_separates_a_dead_end_from_a_recovery() { assert !compiler_refused_and_stopped('./main.v:3:7') } +fn test_compiler_lacks_compatibility_compiler_detects_every_launcher_refusal() { + // These are the single-line refusals `ensure_v1_fallback` prints in the V + // launcher, verbatim. None of them is an "unknown option" line, so before + // this was recognized VLS never retired the lookups and never said anything. + assert compiler_lacks_compatibility_compiler('`-vls-mode` requires the compatibility compiler, but no usable V 0.5.2 fallback was found and make is unavailable. Install make, then run `make v1` in `C:\\Users\\me\\v`.') + assert compiler_lacks_compatibility_compiler('`-vls-mode` requires the compatibility compiler, but the V source tree could not be found. Run `make v1` in the V source directory.') + assert compiler_lacks_compatibility_compiler('`-old-compiler` was requested, but no usable V 0.5.2 fallback was found and make is unavailable. Install make, then run `make v1` in `/home/me/v`.') + assert compiler_lacks_compatibility_compiler('`make v1` failed with exit code 2. Run it manually in `/home/me/v` for more details.') + assert compiler_lacks_compatibility_compiler('`make v1` completed without installing a usable V 0.5.2 fallback at `/home/me/.cache/v1_fallback`.') + // A launcher that recovers on its own only announces the fallback before + // rerunning, then answers. Retiring the lookups on that would cost the + // session every compiler-backed hover, signature, and receiver definition. + assert !compiler_lacks_compatibility_compiler('unknown option `-vls-mode`') + assert !compiler_lacks_compatibility_compiler('unknown option `-vls-mode`\nV compilation failed (compiler_error); retrying with `/v1_fallback`.') + assert !compiler_lacks_compatibility_compiler('`-vls-mode` requires the compatibility compiler, but no usable V 0.5.2 fallback was found; running `make v1` now...\n{"contents":{"kind":"markdown","value":"fn f()"}}') + // A working compiler's payload, an ordinary diagnostic, and empty output. + assert !compiler_lacks_compatibility_compiler('{"contents":{"kind":"markdown","value":"fn f()"}}') + assert !compiler_lacks_compatibility_compiler('./main.v:3:7: error: unknown option') + assert !compiler_lacks_compatibility_compiler('') +} + +// A launcher that understands `-vls-mode` but cannot reach the compatibility +// compiler that implements it, and stops there. Its refusal is not an "unknown +// option" line, which is what made it invisible to the dead-end check. +const no_compat_compiler_launcher_stub = r'#!/bin/sh +echo "\`-vls-mode\` requires the compatibility compiler, but no usable V 0.5.2 fallback was found and make is unavailable. Install make, then run \`make v1\` in \`/v\`." >&2 +exit 1 +' + +fn test_run_v_line_info_retires_lookups_when_the_compatibility_compiler_is_missing() { + $if windows { + // The stand-in launcher is a POSIX shell script. + return + } + previous := os.getenv('VLS_V_COMMAND') + mut app, uri, root := line_info_stub_app('vls_no_compat_compiler', no_compat_compiler_launcher_stub) + defer { + restore_v_command(previous) + os.rmdir_all(root) or {} + } + + assert app.run_v_line_info(.hover, uri, '6:hv^4') == ResponseResult('null') + assert app.line_info_mode == .missing +} + +fn test_report_missing_compatibility_compiler_names_the_repair() { + // The editor otherwise shows a VLS that highlights code but answers nothing + // for completion, hover, signature help, or go to definition, with no hint + // that the compiler is the reason. Platform-independent: no stub compiler. + mut app := App{ + capture_output: true + } + app.report_missing_compatibility_compiler() + assert app.captured_output.len == 1 + assert app.captured_output[0].contains('window/showMessage') + // Name the actual repair, not just the symptom. + assert app.captured_output[0].contains('make v1') + assert app.captured_output[0].contains('type":2') +} + +fn test_a_retired_lookup_never_reports_the_missing_compiler_again() { + // Once the lookups are retired, `run_v_line_info` answers from the index at + // its early `.missing` check, so it never spawns the compiler and never + // re-reports. That structural guarantee is why the notice needs no + // "already warned" flag. + previous := os.getenv('VLS_V_COMMAND') + mut app := App{ + capture_output: true + line_info_mode: .missing + open_files: map[string]string{} + } + uri := 'file:///tmp/vls_retired_lookup.v' + app.open_files[uri] = 'module main\n\n// greet writes a greeting.\nfn greet() {}\n\nfn main() {\n\tgreet()\n}\n' + // A compiler that cannot be spawned at all: reaching it would fail loudly. + os.setenv('VLS_V_COMMAND', os.join_path(os.temp_dir(), 'vls_no_such_compiler'), true) + defer { + restore_v_command(previous) + } + + hover := app.run_v_line_info(.hover, uri, '7:hv^1') + // Answered from the index, from the document's own vdoc comment. + assert hover is Hover + if hover is Hover { + assert hover.contents.value.contains('greet writes a greeting.') + } + // Nothing was said to the client, and no compiler was launched. + assert app.captured_output.len == 0 + assert app.line_info_mode == .missing +} + // line_info_stub_app writes `script` as an executable stand-in for `v`, points // VLS_V_COMMAND at it, and returns an App plus the URI of a lone source file. // The source sits in its own directory so the request takes the single-file @@ -641,12 +731,12 @@ fn test_parse_v_check_diagnostics_reads_v3_output() { diagnostics := parse_v_check_diagnostics(output, '') assert diagnostics.len == 2 assert diagnostics[0] == JsonError{ - path: '/tmp/main.v' + path: '/tmp/main.v' message: 'undefined variable: `missing_name`' line_nr: 4 - col: 7 - len: 12 - level: 'error' + col: 7 + len: 12 + level: 'error' } assert diagnostics[1].level == 'warning' assert diagnostics[1].line_nr == 8 @@ -661,12 +751,12 @@ fn test_parse_v_check_diagnostics_maps_v3_builder_error_to_error() { ' diagnostics := parse_v_check_diagnostics(output, '') assert diagnostics == [JsonError{ - path: '/tmp/main.v' + path: '/tmp/main.v' message: 'cannot import module "missing" (not found)' line_nr: 3 - col: 1 - len: 14 - level: 'error' + col: 1 + len: 14 + level: 'error' }] } @@ -743,8 +833,8 @@ fn test_cache_v_check_result_retries_failure_without_diagnostics() { path := 'file:///tmp/main.v' app.diag_cache[path] = DiagCacheEntry{ content_hash: 1 - generation: 1 - errors: [] + generation: 1 + errors: [] } app.cache_v_check_result(path, 2, 2, [], compiler_exit_timeout, 0) assert path !in app.diag_cache @@ -755,16 +845,16 @@ fn test_cache_v_check_result_retries_timeout_with_partial_diagnostics() { path := 'file:///tmp/main.v' app.diag_cache[path] = DiagCacheEntry{ content_hash: 1 - generation: 1 - errors: [] + generation: 1 + errors: [] } partial_errors := [ JsonError{ - path: '/tmp/main.v' + path: '/tmp/main.v' message: 'partial compiler output' line_nr: 1 - col: 1 - level: 'error' + col: 1 + level: 'error' }, ] app.cache_v_check_result(path, 2, 2, partial_errors, compiler_exit_timeout, partial_errors.len) @@ -799,11 +889,11 @@ fn test_run_v_argv_reports_missing_working_dir() { fn test_v_error_to_lsp_diagnostic_basic() { v_err := JsonError{ - path: '/test/file.v' + path: '/test/file.v' message: 'undefined identifier `foo`' line_nr: 10 - col: 5 - len: 3 + col: 5 + len: 3 } diag := v_error_to_lsp_diagnostic(v_err) @@ -818,11 +908,11 @@ fn test_v_error_to_lsp_diagnostic_basic() { fn test_v_error_to_lsp_diagnostic_first_line() { v_err := JsonError{ - path: '/test/file.v' + path: '/test/file.v' message: 'syntax error' line_nr: 1 - col: 1 - len: 1 + col: 1 + len: 1 } diag := v_error_to_lsp_diagnostic(v_err) @@ -833,11 +923,11 @@ fn test_v_error_to_lsp_diagnostic_first_line() { fn test_v_error_to_lsp_diagnostic_long_error() { v_err := JsonError{ - path: '/test/file.v' + path: '/test/file.v' message: 'unexpected token' line_nr: 100 - col: 50 - len: 20 + col: 50 + len: 20 } diag := v_error_to_lsp_diagnostic(v_err) @@ -848,11 +938,11 @@ fn test_v_error_to_lsp_diagnostic_long_error() { fn test_v_error_to_lsp_diagnostic_zero_length() { v_err := JsonError{ - path: '/test/file.v' + path: '/test/file.v' message: 'error at position' line_nr: 5 - col: 10 - len: 0 + col: 10 + len: 0 } diag := v_error_to_lsp_diagnostic(v_err) @@ -862,11 +952,11 @@ fn test_v_error_to_lsp_diagnostic_zero_length() { fn test_v_error_to_lsp_diagnostic_large_line_numbers() { v_err := JsonError{ - path: '/test/file.v' + path: '/test/file.v' message: 'error in large file' line_nr: 10000 - col: 200 - len: 50 + col: 200 + len: 50 } diag := v_error_to_lsp_diagnostic(v_err) @@ -877,11 +967,11 @@ fn test_v_error_to_lsp_diagnostic_large_line_numbers() { fn test_v_error_to_lsp_diagnostic_column_one() { v_err := JsonError{ - path: '/test/file.v' + path: '/test/file.v' message: 'error at start of line' line_nr: 5 - col: 1 - len: 5 + col: 1 + len: 5 } diag := v_error_to_lsp_diagnostic(v_err) @@ -917,7 +1007,7 @@ fn test_lsp_range_struct() { line: 0 char: 0 } - end: Position{ + end: Position{ line: 0 char: 10 } @@ -932,7 +1022,7 @@ fn test_lsp_range_multiline() { line: 5 char: 10 } - end: Position{ + end: Position{ line: 10 char: 5 } @@ -942,17 +1032,17 @@ fn test_lsp_range_multiline() { fn test_lsp_diagnostic_struct() { diag := LSPDiagnostic{ - range: LSPRange{ + range: LSPRange{ start: Position{ line: 5 char: 0 } - end: Position{ + end: Position{ line: 5 char: 10 } } - message: 'test error' + message: 'test error' severity: 1 } assert diag.message == 'test error' @@ -965,8 +1055,8 @@ fn test_lsp_diagnostic_severities() { severities := [1, 2, 3, 4] // Error, Warning, Information, Hint for sev in severities { diag := LSPDiagnostic{ - range: LSPRange{} - message: 'test' + range: LSPRange{} + message: 'test' severity: sev } assert diag.severity == sev @@ -975,13 +1065,13 @@ fn test_lsp_diagnostic_severities() { fn test_location_struct() { loc := Location{ - uri: 'file:///test/file.v' + uri: 'file:///test/file.v' range: LSPRange{ start: Position{ line: 10 char: 5 } - end: Position{ + end: Position{ line: 10 char: 15 } @@ -999,9 +1089,9 @@ fn test_location_empty() { fn test_detail_struct() { detail := Detail{ - kind: 6 // Function - label: 'my_function' - detail: 'fn my_function() string' + kind: 6 // Function + label: 'my_function' + detail: 'fn my_function() string' documentation: 'A helper function' } assert detail.kind == 6 @@ -1014,7 +1104,7 @@ fn test_detail_kinds() { kinds := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // Text, Method, Function, etc. for k in kinds { detail := Detail{ - kind: k + kind: k label: 'test' } assert detail.kind == k @@ -1023,11 +1113,11 @@ fn test_detail_kinds() { fn test_detail_struct_with_snippet() { detail := Detail{ - kind: 6 - label: 'println' - detail: 'fn println(s string)' - documentation: 'Prints a string' - insert_text: 'println(\${1:s})' + kind: 6 + label: 'println' + detail: 'fn println(s string)' + documentation: 'Prints a string' + insert_text: 'println(\${1:s})' insert_text_format: 2 // Snippet format } assert detail.insert_text? == 'println(\${1:s})' @@ -1036,7 +1126,7 @@ fn test_detail_struct_with_snippet() { fn test_detail_without_snippet() { detail := Detail{ - kind: 6 + kind: 6 label: 'println' } assert detail.insert_text == none @@ -1045,9 +1135,9 @@ fn test_detail_without_snippet() { fn test_signature_help_struct() { sig := SignatureHelp{ - signatures: [ + signatures: [ SignatureInformation{ - label: 'fn my_func(a int, b string) bool' + label: 'fn my_func(a int, b string) bool' parameters: [ ParameterInformation{ label: 'a int' @@ -1068,7 +1158,7 @@ fn test_signature_help_struct() { fn test_signature_help_multiple_signatures() { sig := SignatureHelp{ - signatures: [ + signatures: [ SignatureInformation{ label: 'fn overload1(a int)' }, @@ -1096,17 +1186,17 @@ fn test_signature_help_empty() { fn test_capabilities_struct() { cap := Capabilities{ capabilities: Capability{ - text_document_sync: TextDocumentSyncOptions{ + text_document_sync: TextDocumentSyncOptions{ open_close: true - change: 1 + change: 1 } - completion_provider: CompletionProvider{ + completion_provider: CompletionProvider{ trigger_characters: ['.'] } signature_help_provider: SignatureHelpOptions{ trigger_characters: ['(', ','] } - definition_provider: true + definition_provider: true } } assert cap.capabilities.definition_provider == true @@ -1127,11 +1217,11 @@ fn test_capabilities_minimal() { fn test_request_struct() { req := Request{ - id: 1 - method: 'textDocument/completion' + id: 1 + method: 'textDocument/completion' jsonrpc: '2.0' - params: json2.encode(Params{ - position: Position{ + params: json2.encode(Params{ + position: Position{ line: 5 char: 10 } @@ -1162,7 +1252,7 @@ fn test_request_params_decode_malformed_returns_error() { fn test_response_struct() { resp := Response{ - id: 1 + id: 1 result: 'null' } assert resp.id == 1 @@ -1171,7 +1261,7 @@ fn test_response_struct() { fn test_response_with_capabilities() { resp := Response{ - id: 0 + id: 0 result: Capabilities{ capabilities: Capability{ definition_provider: true @@ -1188,7 +1278,7 @@ fn test_notification_struct() { notif := Notification{ method: 'textDocument/publishDiagnostics' params: PublishDiagnosticsParams{ - uri: 'file:///test.v' + uri: 'file:///test.v' diagnostics: [] } } @@ -1200,16 +1290,16 @@ fn test_notification_with_diagnostics() { notif := Notification{ method: 'textDocument/publishDiagnostics' params: PublishDiagnosticsParams{ - uri: 'file:///test.v' + uri: 'file:///test.v' diagnostics: [ LSPDiagnostic{ - range: LSPRange{} - message: 'error 1' + range: LSPRange{} + message: 'error 1' severity: 1 }, LSPDiagnostic{ - range: LSPRange{} - message: 'error 2' + range: LSPRange{} + message: 'error 2' severity: 1 }, ] @@ -1252,7 +1342,7 @@ fn test_write_tracked_files_to_temp_single_file() { interop_test_must_write_file(test_file, 'module main') mut app := &App{ - temp_dir: temp_dir + temp_dir: temp_dir open_files: map[string]string{} } @@ -1290,7 +1380,7 @@ fn test_write_tracked_files_to_temp_multiple_files() { } mut app := &App{ - temp_dir: temp_dir + temp_dir: temp_dir open_files: map[string]string{} } @@ -1331,7 +1421,7 @@ fn test_write_tracked_files_to_temp_nested_directories() { interop_test_must_write_file(nested_file, 'module internal') mut app := &App{ - temp_dir: temp_dir + temp_dir: temp_dir open_files: map[string]string{} } @@ -1371,7 +1461,7 @@ fn test_prepare_compilation_overlay_preserves_nested_symlink_layout() { main_uri := path_to_uri(main_file) unsaved_content := 'module main\n\nfn unsaved() {}\n' mut app := &App{ - temp_dir: app_temp_dir + temp_dir: app_temp_dir open_files: { main_uri: unsaved_content } @@ -1412,7 +1502,7 @@ fn test_prepare_compilation_overlay_preserves_posix_backslashes() { assert uri_to_path(main_uri) == main_file unsaved_content := 'module main\n\nfn unsaved() {}\n' mut app := &App{ - temp_dir: app_temp_dir + temp_dir: app_temp_dir open_files: { main_uri: unsaved_content } @@ -1450,7 +1540,7 @@ fn test_write_tracked_files_skips_files_outside_working_dir() { interop_test_must_write_file(other_file, 'module other') mut app := &App{ - temp_dir: temp_dir + temp_dir: temp_dir open_files: map[string]string{} } @@ -1911,8 +2001,8 @@ fn test_json_error_negative_values() { // to 0 rather than emitted as negative positions (P1-09). err := JsonError{ line_nr: -1 - col: -1 - len: -1 + col: -1 + len: -1 } diag := v_error_to_lsp_diagnostic(err) assert diag.severity == 1 @@ -1939,11 +2029,11 @@ fn test_params_struct_complete() { content_changes: [ContentChange{ text: 'test' }] - position: Position{ + position: Position{ line: 5 char: 10 } - text_document: TextDocumentIdentifier{ + text_document: TextDocumentIdentifier{ uri: 'file:///test.v' } } @@ -1971,7 +2061,7 @@ fn test_signature_help_options_triggers() { fn test_text_document_sync_options() { sync := TextDocumentSyncOptions{ open_close: true - change: 1 // Full sync + change: 1 // Full sync } assert sync.open_close == true assert sync.change == 1 @@ -1980,7 +2070,7 @@ fn test_text_document_sync_options() { fn test_text_document_sync_incremental() { sync := TextDocumentSyncOptions{ open_close: true - change: 2 // Incremental sync + change: 2 // Incremental sync } assert sync.change == 2 } @@ -1994,7 +2084,7 @@ fn test_parameter_information() { fn test_signature_information_with_params() { sig := SignatureInformation{ - label: 'fn test(a int, b string, c bool)' + label: 'fn test(a int, b string, c bool)' parameters: [ ParameterInformation{ label: 'a int' @@ -2013,11 +2103,11 @@ fn test_signature_information_with_params() { fn test_publish_diagnostics_params() { params := PublishDiagnosticsParams{ - uri: 'file:///test.v' + uri: 'file:///test.v' diagnostics: [ LSPDiagnostic{ - range: LSPRange{} - message: 'error' + range: LSPRange{} + message: 'error' severity: 1 }, ] From e50666628b827eb21b96f4611e8ec38bcc238593 Mon Sep 17 00:00:00 2001 From: metif12 Date: Sat, 3 Oct 2026 17:07:54 +0330 Subject: [PATCH 2/7] test: recognise the reworded "make is unavailable" refusal too vlang/v#29369 (merged as 76d88b9) rewrites the tail of the launcher's refusal: the sentence after "make is unavailable" is now a platform-specific hint, and the comma became a period. On Windows the clause reads On Windows, install GNU make in MSYS2 (`make` or `mingw32-make`) and put its tools, including `sh`, on PATH. and elsewhere it is still "Install make.". `compiler_lacks_compatibility_compiler` keys on "requires the compatibility compiler", which both spellings contain, so detection is unaffected. This test pinned the old wording verbatim, so it now asserts both: green before the compiler change and after it, and the coupling is written down instead of being rediscovered the next time the wording shifts. Validated against V 0.5.2 76d88b9, the merged compiler: the Windows sample above is its output verbatim. `interop_test.v` passes; the module suite reports the same 3 failures with and without this change - `index_test.v`, `handlers_test.v` and `integration_test.v`, none of them in this file. `integration_test.v` fails on an empty completion list, which is the symptom of the missing V1 fallback this refusal describes, and is what vlang/v#29369 and vlang/vscode-vlang#543 are about. --- interop_test.v | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/interop_test.v b/interop_test.v index 86308622..cdab37f3 100644 --- a/interop_test.v +++ b/interop_test.v @@ -476,8 +476,15 @@ fn test_compiler_lacks_compatibility_compiler_detects_every_launcher_refusal() { // launcher, verbatim. None of them is an "unknown option" line, so before // this was recognized VLS never retired the lookups and never said anything. assert compiler_lacks_compatibility_compiler('`-vls-mode` requires the compatibility compiler, but no usable V 0.5.2 fallback was found and make is unavailable. Install make, then run `make v1` in `C:\\Users\\me\\v`.') + // vlang/v#29369 rewrote the tail of that refusal: the hint after "make is + // unavailable" is now platform specific, and the sentence is split. Detection + // keys on "requires the compatibility compiler", so both spellings must retire + // the lookups, and this test has to hold either side of that change. + assert compiler_lacks_compatibility_compiler('`-vls-mode` requires the compatibility compiler, but no usable V 0.5.2 fallback was found and make is unavailable. On Windows, install GNU make in MSYS2 (`make` or `mingw32-make`) and put its tools, including `sh`, on PATH. Then run `make v1` in `C:\\Users\\me\\v`.') assert compiler_lacks_compatibility_compiler('`-vls-mode` requires the compatibility compiler, but the V source tree could not be found. Run `make v1` in the V source directory.') assert compiler_lacks_compatibility_compiler('`-old-compiler` was requested, but no usable V 0.5.2 fallback was found and make is unavailable. Install make, then run `make v1` in `/home/me/v`.') + // The same rewrite as above, on the host where the hint is the short one. + assert compiler_lacks_compatibility_compiler('`-old-compiler` was requested, but no usable V 0.5.2 fallback was found and make is unavailable. Install make. Then run `make v1` in `/home/me/v`.') assert compiler_lacks_compatibility_compiler('`make v1` failed with exit code 2. Run it manually in `/home/me/v` for more details.') assert compiler_lacks_compatibility_compiler('`make v1` completed without installing a usable V 0.5.2 fallback at `/home/me/.cache/v1_fallback`.') // A launcher that recovers on its own only announces the fallback before From 3d3ba5d1b655b2326ad0948a3c73b3e0ba9b1710 Mon Sep 17 00:00:00 2001 From: metif12 Date: Wed, 7 Oct 2026 10:14:48 +0330 Subject: [PATCH 3/7] fix: correct compile target and path resolution for multifile diagnostics - Use compile_target as exec_dir and diagnostic_source_dir so the compiler checks the correct module directory - Add base_dir parameter to source_path_from_overlay so relative paths resolve against the compile target, not the project root - Escape backslashes in JSON rootPath for Windows paths - Fix u64 to int conversion in test file size calculations --- handlers_test.v | 2 +- index_test.v | 4 ++-- integration_test.v | 5 +++-- interop.v | 17 +++++++++-------- interop_test.v | 6 +++--- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/handlers_test.v b/handlers_test.v index c5a8cdaf..2f0f9ede 100644 --- a/handlers_test.v +++ b/handlers_test.v @@ -3765,7 +3765,7 @@ fn test_handle_rename_refuses_incomplete_oversized_sibling_index() { test_file := os.join_path(test_dir, 'main.v') content := 'module main\n\nfn target() {\n\ttarget()\n}\n' must_write_file(test_file, content) - must_write_file(os.join_path(test_dir, 'oversized.v'), 'x'.repeat(index_max_file_bytes + 1)) + must_write_file(os.join_path(test_dir, 'oversized.v'), 'x'.repeat(int(index_max_file_bytes) + 1)) uri := path_to_uri(test_file) app.open_files[uri] = content diff --git a/index_test.v b/index_test.v index 3f459705..d78354b0 100644 --- a/index_test.v +++ b/index_test.v @@ -114,7 +114,7 @@ fn test_watched_file_reindex_drops_oversized_disk_entry() { app.occurrences_for(uri) assert uri in app.ref_occurrences - os.write_file(path, 'x'.repeat(index_max_file_bytes + 1)) or { + os.write_file(path, 'x'.repeat(int(index_max_file_bytes) + 1)) or { assert false, 'grow watched file failed: ${err}' return } @@ -816,7 +816,7 @@ fn test_index_completeness_is_scoped_to_relevant_project() { return } path_b := os.join_path(root_b, 'oversized.v') - os.write_file(path_b, 'x'.repeat(index_max_file_bytes + 1)) or { + os.write_file(path_b, 'x'.repeat(int(index_max_file_bytes) + 1)) or { assert false, 'write oversized root_b file failed: ${err}' return } diff --git a/integration_test.v b/integration_test.v index d3410bdf..f44ac71f 100644 --- a/integration_test.v +++ b/integration_test.v @@ -2793,13 +2793,14 @@ fn test_integration_sublime_text_lsp_handshake() { cleanup_integration_test_env(app, project_dir) } root_uri := path_to_uri(project_dir) - initialize := '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":42,"clientInfo":{"name":"Sublime Text LSP","version":"2.13.0"},"locale":"en","rootUri":"${root_uri}","rootPath":"${project_dir}","workspaceFolders":[{"uri":"${root_uri}","name":"test_project"}],"capabilities":{"general":{"positionEncodings":["utf-16"]},"workspace":{"workspaceFolders":true,"configuration":true,"didChangeWatchedFiles":{"dynamicRegistration":true,"relativePatternSupport":true}},"textDocument":{"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"completionItem":{"snippetSupport":true,"documentationFormat":["markdown","plaintext"]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"publishDiagnostics":{"versionSupport":true}},"window":{"workDoneProgress":true}},"initializationOptions":{}}}' + json_root_path := project_dir.replace('\\', '\\\\') + initialize := '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":42,"clientInfo":{"name":"Sublime Text LSP","version":"2.13.0"},"locale":"en","rootUri":"${root_uri}","rootPath":"${json_root_path}","workspaceFolders":[{"uri":"${root_uri}","name":"test_project"}],"capabilities":{"general":{"positionEncodings":["utf-16"]},"workspace":{"workspaceFolders":true,"configuration":true,"didChangeWatchedFiles":{"dynamicRegistration":true,"relativePatternSupport":true}},"textDocument":{"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"completionItem":{"snippetSupport":true,"documentationFormat":["markdown","plaintext"]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"publishDiagnostics":{"versionSupport":true}},"window":{"workDoneProgress":true}},"initializationOptions":{}}}' initialized := '{"jsonrpc":"2.0","method":"initialized","params":{}}' output := integration_run_frames(mut app, project_dir, 'sublime_handshake', [initialize, initialized]) assert app.received_initialize - assert app.workspace_roots == [project_dir] + assert app.workspace_roots == [project_dir.replace('\\', '/')] assert app.position_encoding == .utf16 assert app.supports_dynamic_watched_files_registration assert app.supports_work_done_progress diff --git a/interop.v b/interop.v index 5720d7b5..06730da9 100644 --- a/interop.v +++ b/interop.v @@ -790,12 +790,13 @@ fn (mut app App) prepare_compilation_overlay(real_path string) !CompilationOverl // source_path_from_overlay maps compiler paths in the temporary project back to // their original source paths. -fn source_path_from_overlay_with_windows_rules(reported_path string, overlay CompilationOverlay, windows bool) string { +fn source_path_from_overlay_with_windows_rules(reported_path string, overlay CompilationOverlay, windows bool, base_dir string) string { mut candidate := normalize_overlay_path_with_windows_rules(reported_path, windows) + resolve_base := if base_dir != '' { base_dir } else { overlay.temp_work_dir } if candidate.starts_with('./') || candidate.starts_with('.\\') { - candidate = os.join_path(overlay.temp_work_dir, candidate[2..]) + candidate = os.join_path(resolve_base, candidate[2..]) } else if !os.is_abs_path(candidate) { - candidate = os.join_path(overlay.temp_work_dir, candidate) + candidate = os.join_path(resolve_base, candidate) } candidate = normalize_overlay_path_with_windows_rules(candidate, windows) temp_root := normalize_overlay_path_with_windows_rules(overlay.temp_root, windows) @@ -806,11 +807,11 @@ fn source_path_from_overlay_with_windows_rules(reported_path string, overlay Com return candidate } -fn source_path_from_overlay(reported_path string, overlay CompilationOverlay) string { +fn source_path_from_overlay(reported_path string, overlay CompilationOverlay, base_dir string) string { $if windows { - return source_path_from_overlay_with_windows_rules(reported_path, overlay, true) + return source_path_from_overlay_with_windows_rules(reported_path, overlay, true, base_dir) } - return source_path_from_overlay_with_windows_rules(reported_path, overlay, false) + return source_path_from_overlay_with_windows_rules(reported_path, overlay, false, base_dir) } fn (mut app App) run_v_check(path string, text string) []JsonError { @@ -888,7 +889,7 @@ fn (mut app App) run_v_check(path string, text string) []JsonError { mut filtered_errors := []JsonError{} for err in v_errors { - err_file := source_path_from_overlay(err.path, overlay) + err_file := source_path_from_overlay(err.path, overlay, compile_target) if normalized_index_path(err_file) == normalized_index_path(real_path) { updated_err := JsonError{ path: real_path @@ -1777,7 +1778,7 @@ fn (mut app App) run_v_line_info(method Method, path string, line_info string) R col := fields[fields.len - 1].int() mut uri_path := os.to_slash(fields[..fields.len - 2].join(':')) if use_multifile && temp_project_dir != '' { - uri_path = source_path_from_overlay(uri_path, overlay) + uri_path = source_path_from_overlay(uri_path, overlay, compile_target) log('MAPPED TO uri_path=${uri_path}') } // Build a proper percent-encoded DocumentUri so paths containing diff --git a/interop_test.v b/interop_test.v index cdab37f3..bc01ea3a 100644 --- a/interop_test.v +++ b/interop_test.v @@ -167,7 +167,7 @@ fn test_source_path_from_overlay_normalizes_windows_relative_join() { temp_root: r'C:\temp\overlay' temp_work_dir: r'C:\temp\overlay\src' } - mapped := source_path_from_overlay_with_windows_rules('./main.v', overlay, true) + mapped := source_path_from_overlay_with_windows_rules('./main.v', overlay, true, '') assert mapped == 'C:/repo/src/main.v' } @@ -1488,7 +1488,7 @@ fn test_prepare_compilation_overlay_preserves_nested_symlink_layout() { assert overlay.temp_work_dir == os.join_path(overlay.temp_root, 'src') assert overlay.temp_source_file == os.join_path(overlay.temp_root, 'src', 'main.v') assert os.read_file(overlay.temp_source_file) or { '' } == unsaved_content - mapped_path := source_path_from_overlay(overlay.temp_source_file, overlay) + mapped_path := source_path_from_overlay(overlay.temp_source_file, overlay, '') assert normalize_overlay_path(mapped_path) == normalize_overlay_path(main_file) } @@ -1526,7 +1526,7 @@ fn test_prepare_compilation_overlay_preserves_posix_backslashes() { assert overlay.source_root == project_dir assert overlay.source_display_root == project_dir assert os.read_file(overlay.temp_source_file) or { '' } == unsaved_content - mapped_path := source_path_from_overlay(overlay.temp_source_file, overlay) + mapped_path := source_path_from_overlay(overlay.temp_source_file, overlay, '') assert mapped_path == main_file } } From 8cf8b10032fb1893b7696a84409b540cdff83531 Mon Sep 17 00:00:00 2001 From: metif12 Date: Wed, 7 Oct 2026 10:57:52 +0330 Subject: [PATCH 4/7] fix: resolve Windows launcher shim to the real V compiler When the 'v' on PATH is a forwarding wrapper (v.bat -> v.exe), find_v_dir returned the wrapper directory, so vlib module lookups (os, builtin, time, strings) resolved nowhere and indexed member completions came back empty. Unwrap .bat/.cmd shims to their quoted executable target in resolve_v_compiler_exe and cover it with tests. --- integration_test.v | 4 +-- interop.v | 44 ++++++++++++++++++++++++++- interop_test.v | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/integration_test.v b/integration_test.v index f44ac71f..f8e187bf 100644 --- a/integration_test.v +++ b/integration_test.v @@ -59,9 +59,9 @@ fn create_integration_test_env() (&App, string) { integration_test_must_mkdir_all(project_dir) app := &App{ - text: '' + text: '' open_files: map[string]string{} - temp_dir: temp_dir + temp_dir: temp_dir } return app, project_dir } diff --git a/interop.v b/interop.v index 06730da9..c798b8f0 100644 --- a/interop.v +++ b/interop.v @@ -12,9 +12,51 @@ import time fn resolve_v_compiler_exe() string { configured := os.getenv('VLS_V_COMMAND').trim_space() if configured != '' { + unwrapped := resolve_wrapper_target(configured) + if unwrapped != '' { + return unwrapped + } return configured } - return os.find_abs_path_of_executable('v') or { 'v' } + found := os.find_abs_path_of_executable('v') or { 'v' } + unwrapped := resolve_wrapper_target(found) + if unwrapped != '' { + return unwrapped + } + return found +} + +// resolve_wrapper_target unwraps a Windows launcher shim (a `.bat`/`.cmd` +// file that forwards to the real compiler executable) so compiler-relative +// lookups such as the vlib directory resolve against the real installation +// instead of the shim's directory. It returns '' when `candidate` is not a +// wrapper or no usable target is found, in which case the caller keeps the +// original path. +fn resolve_wrapper_target(candidate string) string { + if candidate == '' || candidate == 'v' { + return '' + } + lower := candidate.to_lower() + if !(lower.ends_with('.bat') || lower.ends_with('.cmd')) { + return '' + } + content := os.read_file(candidate) or { return '' } + for line in content.split_into_lines() { + trimmed := line.trim_space() + if !trimmed.starts_with('"') { + continue + } + end := trimmed[1..].index('"') or { continue } + target := trimmed[1..end + 1] + target_lower := target.to_lower() + if target_lower.ends_with('.bat') || target_lower.ends_with('.cmd') { + continue + } + if os.is_file(target) { + return target + } + } + return '' } // compiler_is_available reports whether the V compiler was resolved to a real diff --git a/interop_test.v b/interop_test.v index bc01ea3a..5d817ff7 100644 --- a/interop_test.v +++ b/interop_test.v @@ -38,6 +38,80 @@ fn test_resolve_v_compiler_exe_prefers_configured_command() { assert resolve_v_compiler_exe() == configured } +fn test_resolve_wrapper_target_ignores_plain_executable() { + assert resolve_wrapper_target('C:\\v\\v.exe') == '' + assert resolve_wrapper_target('/usr/local/bin/v') == '' + assert resolve_wrapper_target('') == '' + assert resolve_wrapper_target('v') == '' +} + +fn test_resolve_wrapper_target_ignores_missing_wrapper() { + missing := os.join_path(os.temp_dir(), 'vls_no_such_wrapper_${os.getpid()}.bat') + assert resolve_wrapper_target(missing) == '' +} + +fn test_resolve_wrapper_target_unwraps_bat_forwarding_to_exe() { + base := os.join_path(os.temp_dir(), 'vls_wrapper_probe_${os.getpid()}') + bin := os.join_path(base, '.bin') + interop_test_must_mkdir_all(bin) + defer { + os.rmdir_all(base) or {} + } + target := os.join_path(base, 'v.exe') + interop_test_must_write_file(target, '') + wrapper := os.join_path(bin, 'v.bat') + interop_test_must_write_file(wrapper, '@echo off\n"${target}" %*\n') + assert resolve_wrapper_target(wrapper) == target +} + +fn test_resolve_wrapper_target_ignores_bat_without_quoted_target() { + base := os.join_path(os.temp_dir(), 'vls_wrapper_bare_${os.getpid()}') + interop_test_must_mkdir_all(base) + defer { + os.rmdir_all(base) or {} + } + wrapper := os.join_path(base, 'v.bat') + interop_test_must_write_file(wrapper, '@echo off\nv.exe %*\n') + assert resolve_wrapper_target(wrapper) == '' +} + +fn test_resolve_wrapper_target_ignores_wrapper_chain() { + base := os.join_path(os.temp_dir(), 'vls_wrapper_chain_${os.getpid()}') + interop_test_must_mkdir_all(base) + defer { + os.rmdir_all(base) or {} + } + inner := os.join_path(base, 'inner.bat') + interop_test_must_write_file(inner, '@echo off\necho hi\n') + outer := os.join_path(base, 'outer.bat') + interop_test_must_write_file(outer, '@echo off\n"${inner}" %*\n') + assert resolve_wrapper_target(outer) == '' +} + +fn test_resolve_v_compiler_exe_unwraps_configured_wrapper() { + old_command := os.getenv('VLS_V_COMMAND') + defer { + if old_command == '' { + os.unsetenv('VLS_V_COMMAND') + } else { + os.setenv('VLS_V_COMMAND', old_command, true) + } + } + base := os.join_path(os.temp_dir(), 'vls_wrapper_env_${os.getpid()}') + bin := os.join_path(base, '.bin') + interop_test_must_mkdir_all(bin) + defer { + os.rmdir_all(base) or {} + } + target := os.join_path(base, 'v.exe') + interop_test_must_write_file(target, '') + wrapper := os.join_path(bin, 'v.bat') + interop_test_must_write_file(wrapper, '@echo off\n"${target}" %*\n') + os.setenv('VLS_V_COMMAND', wrapper, true) + assert resolve_v_compiler_exe() == target + assert find_v_dir() == os.dir(os.real_path(target)) +} + // --- uri_to_path tests --- fn test_uri_to_path_unix_style() { From 51981a6b3108b70a59bc1952016904763b6e689f Mon Sep 17 00:00:00 2001 From: metif12 Date: Wed, 7 Oct 2026 22:11:03 +0330 Subject: [PATCH 5/7] Fix merge fallout: drop stray braces and thread compile_target through split_check_errors --- interop.v | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/interop.v b/interop.v index b4b19987..ff45b169 100644 --- a/interop.v +++ b/interop.v @@ -1401,9 +1401,9 @@ fn (mut app App) run_v_check(path string, text string) []JsonError { // check: they are shown at once (see DiagnosticsServer.ask). publish := app.diagnostics_partial partial := if publish != unsafe { nil } { - fn [publish, diagnostic_source_dir, file_to_check, use_multifile, overlay, real_path, program_uris, path] (answer os.Result) { + fn [publish, diagnostic_source_dir, file_to_check, use_multifile, overlay, real_path, program_uris, compile_target, path] (answer os.Result) { found := split_check_errors(answer.output, diagnostic_source_dir, file_to_check, - use_multifile, overlay, real_path, program_uris) + use_multifile, overlay, real_path, program_uris, compile_target) publish(path, found) } } else { @@ -1435,7 +1435,7 @@ fn (mut app App) run_v_check(path string, text string) []JsonError { } found := split_check_errors(x.output, diagnostic_source_dir, file_to_check, use_multifile, - overlay, real_path, program_uris) + overlay, real_path, program_uris, compile_target) if server_exe == '' { cleanup_compilation_temp(temp_project_dir, singlefile_tmppath) } @@ -1481,7 +1481,7 @@ fn (app &App) program_open_files(real_path string, overlay CompilationOverlay) m // split_check_errors reads the diagnostics of `output`, what a check printed, // for the file at `real_path`, and, when the check covered its program, for the // other open files of the program, `program_uris` (see program_open_files). -fn split_check_errors(output string, diagnostic_source_dir string, file_to_check string, use_multifile bool, overlay CompilationOverlay, real_path string, program_uris map[string]string) CheckErrors { +fn split_check_errors(output string, diagnostic_source_dir string, file_to_check string, use_multifile bool, overlay CompilationOverlay, real_path string, program_uris map[string]string, base_dir string) CheckErrors { // Parse V3's native flat-AST checker diagnostics so ordinary diagnostics stay // on the default backend. mut v_errors := parse_v_check_diagnostics(output, diagnostic_source_dir) @@ -1498,7 +1498,7 @@ fn split_check_errors(output string, diagnostic_source_dir string, file_to_check } mut filtered_errors := []JsonError{} for err in v_errors { - err_file := source_path_from_overlay(err.path, overlay) + err_file := source_path_from_overlay(err.path, overlay, base_dir) if normalized_index_path(err_file) == normalized_index_path(real_path) { filtered_errors << JsonError{ path: real_path @@ -2539,8 +2539,6 @@ fn (mut app App) line_info_result(method Method, path string, line_info string, result = hints } } - } - } } .definition, .declaration, .type_definition, .implementation { // file.v:line:col => Location @@ -2563,7 +2561,6 @@ fn (mut app App) line_info_result(method Method, path string, line_info string, result = app.compiler_location(uri_path, line_nr, col) } } - else {} } return result From a9ae45063a0144fca9a23aaced7b46fec0533515 Mon Sep 17 00:00:00 2001 From: metif12 Date: Thu, 8 Oct 2026 08:28:55 +0330 Subject: [PATCH 6/7] Fix undefined compile_target: thread it through line_info_result and update source_path_from_overlay callers --- interop.v | 4 ++-- rename_conflicts.v | 3 ++- v3_line_info.v | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/interop.v b/interop.v index ff45b169..7c248eda 100644 --- a/interop.v +++ b/interop.v @@ -2482,13 +2482,13 @@ fn (mut app App) run_v_line_info_once(method Method, path string, line_info stri log('RUN RES ${x}') return app.line_info_result(method, path, line_info, output, use_multifile, temp_project_dir, - overlay) + overlay, compile_target) } // line_info_result turns what the compiler printed into the answer the client // expects. Both the one-shot process and the persistent compiler end here, so // the two paths cannot drift apart. -fn (mut app App) line_info_result(method Method, path string, line_info string, output string, use_multifile bool, temp_project_dir string, overlay CompilationOverlay) ResponseResult { +fn (mut app App) line_info_result(method Method, path string, line_info string, output string, use_multifile bool, temp_project_dir string, overlay CompilationOverlay, compile_target string) ResponseResult { // Default to JSON null so any unhandled method branch produces a valid LSP response. mut result := ResponseResult('null') match method { diff --git a/rename_conflicts.v b/rename_conflicts.v index 6667d616..551f7ba1 100644 --- a/rename_conflicts.v +++ b/rename_conflicts.v @@ -345,7 +345,8 @@ fn parse_name_pos(answer string, overlay CompilationOverlay) ?NamePos { || !fields[fields.len - 2].is_int() { return none } - path := source_path_from_overlay(os.to_slash(fields[..fields.len - 2].join(':')), overlay) + path := source_path_from_overlay(os.to_slash(fields[..fields.len - 2].join(':')), overlay, + overlay.temp_work_dir) return NamePos{ path: normalize_overlay_path(path) line: fields[fields.len - 2].int() - 1 diff --git a/v3_line_info.v b/v3_line_info.v index a74ba5f1..b2a9ef66 100644 --- a/v3_line_info.v +++ b/v3_line_info.v @@ -79,7 +79,7 @@ fn (mut app App) v3_line_info(method Method, path string, real_path string, line } log('V3 answered ${output.len} bytes') return app.line_info_result(method, path, line_info, output, true, result.project.overlay.temp_root, - result.project.overlay) + result.project.overlay, result.project.overlay.temp_work_dir) } // V3Hover is what V3 answers for a hover: what it says of the name under the @@ -117,7 +117,8 @@ fn (mut app App) v3_hover(uri string, real_path string, line_info string) ?V3Hov mut declared_at := ?Location(none) if definition != '' { located := app.line_info_result(.definition, uri, definition_info, definition, - true, result.project.overlay.temp_root, result.project.overlay) + true, result.project.overlay.temp_root, result.project.overlay, + result.project.overlay.temp_work_dir) if located is Location { declared_at = located } @@ -510,7 +511,8 @@ fn (mut app App) v3_prefetch_anchors(locations []Location, mut cache map[string] continue } found := app.line_info_result(.definition, loc.uri, questions[i].line_info, output, - true, result.project.overlay.temp_root, result.project.overlay) + true, result.project.overlay.temp_root, result.project.overlay, + result.project.overlay.temp_work_dir) if found is Location && found.uri != '' { cache[anchor_cache_key(loc.uri, loc.range.start.line, loc.range.start.char)] = found } From 13760c30128862a2683f70712e627ada6b32f4b4 Mon Sep 17 00:00:00 2001 From: metif12 Date: Thu, 8 Oct 2026 22:42:52 +0330 Subject: [PATCH 7/7] test: give the wrapper fixture the vlib dir a real install has --- interop_test.v | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interop_test.v b/interop_test.v index e10c7a7b..6fdd66ee 100644 --- a/interop_test.v +++ b/interop_test.v @@ -105,6 +105,9 @@ fn test_resolve_v_compiler_exe_unwraps_configured_wrapper() { } target := os.join_path(base, 'v.exe') interop_test_must_write_file(target, '') + // A real V installation holds vlib next to the compiler; without it the + // walk-up has no V home to find. + interop_test_must_mkdir_all(os.join_path(base, 'vlib')) wrapper := os.join_path(bin, 'v.bat') interop_test_must_write_file(wrapper, '@echo off\n"${target}" %*\n') os.setenv('VLS_V_COMMAND', wrapper, true)