diff --git a/interop.v b/interop.v index 3993cc31..7c248eda 100644 --- a/interop.v +++ b/interop.v @@ -13,9 +13,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 @@ -1204,12 +1246,13 @@ fn (mut app App) prepare_compilation_overlay_with(real_path string, work_dir str // 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) @@ -1220,11 +1263,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 { @@ -1358,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 { @@ -1392,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) } @@ -1438,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) @@ -1455,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 @@ -2439,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 { @@ -2508,7 +2551,7 @@ fn (mut app App) line_info_result(method Method, path string, line_info string, 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 @@ -2518,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 diff --git a/interop_test.v b/interop_test.v index 8f8e13dc..6fdd66ee 100644 --- a/interop_test.v +++ b/interop_test.v @@ -38,6 +38,83 @@ 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, '') + // 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) + 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() { @@ -167,7 +244,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' } @@ -1716,7 +1793,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) } @@ -1754,7 +1831,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 } } 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 }