Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion handlers_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions index_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 5 additions & 4 deletions integration_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
165 changes: 130 additions & 35 deletions interop.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -267,7 +309,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 {
Expand All @@ -284,8 +327,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
}

Expand Down Expand Up @@ -338,6 +381,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.
Expand Down Expand Up @@ -465,11 +558,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
Expand Down Expand Up @@ -517,8 +610,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
}
}

Expand Down Expand Up @@ -553,7 +646,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()
Expand Down Expand Up @@ -615,12 +708,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()
}
}

Expand Down Expand Up @@ -728,23 +821,24 @@ 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)
}
}

// 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 @@ -755,11 +849,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 @@ -837,15 +931,15 @@ 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
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}')
Expand Down Expand Up @@ -1513,7 +1607,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
}
}
Expand Down Expand Up @@ -1630,7 +1724,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
Expand All @@ -1639,6 +1733,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)
}
Expand Down Expand Up @@ -1696,15 +1791,15 @@ 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
}
}
} else if doc != '' {
// Compiler returned no info but we found a vdoc comment
result = Hover{
contents: MarkupContent{
kind: 'markdown'
kind: 'markdown'
value: doc
}
}
Expand All @@ -1725,7 +1820,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
Expand All @@ -1749,13 +1844,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
}
Expand Down
Loading