Skip to content
74 changes: 58 additions & 16 deletions interop.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
83 changes: 80 additions & 3 deletions interop_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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'
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}
}
Expand Down
3 changes: 2 additions & 1 deletion rename_conflicts.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions v3_line_info.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down