Skip to content
Merged
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
129 changes: 20 additions & 109 deletions handlers.v
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
// Copyright (c) 2025 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by a GPL license that can be found in the LICENSE file.

// `user. ...` fields and methods
fn (mut app App) completion(request Request) Response {
log('completion')
log('request=${request}')
// app.text = request.params.content_changes[0].text
path := request.params.text_document.uri
fn (mut app App) operation_at_pos(method Method, request Request) Response {
line_nr := request.params.position.line + 1
col := request.params.position.char
var_ac := app.run_v_line_info(path, line_nr, col)
log('var_ac=${var_ac}')
resp := Response{
path := request.params.text_document.uri
line_info := match method {
.completion {
'${line_nr}:${col}'
}
.signature_help {
'${line_nr}:fn^${col}'
}
.definition {
'${line_nr}:gd^${col}'
}
else {
''
}
}
result := app.run_v_line_info(method, path, line_info)
log(result.str())
return Response{
id: request.id
result: var_ac.details
result: result
}
return resp
}

// Returns instant red wavy errors
Expand Down Expand Up @@ -50,101 +59,3 @@ fn (mut app App) on_did_change(request Request) ?Notification {
log('returning notification: ${notification}')
return notification
}

// Autocomplete for `os.create(...`
// Function parameters, with currently typed parameter being highlighted
fn (mut app App) signature_help(request Request) Response {
// For signature help, the file must be up-to-date.
path := request.params.text_document.uri
lines := app.text.split('\n')
line_nr := request.params.position.line
char_pos := request.params.position.char
if line_nr >= lines.len {
return Response{
id: request.id
result: 'null'
}
}
line_text := lines[line_nr]
log('SIG LINE TEXT=${line_text}')
signature_help := app.run_v_fn_sig(path, line_nr, char_pos)
return Response{
id: request.id
result: signature_help
}
}

// Finds the word/expression at a given cursor position in the document.
fn (app &App) get_expression_at_cursor(line_nr int, col int) string {
log('get_expression_at_cursor line_nr=${line_nr} col=${col}')
lines := app.text.split('\n')
if line_nr < 0 || line_nr >= lines.len {
return ''
}
line := lines[line_nr].trim_space()
log('EXPR LINE="${line}"')
if col < 0 || col > line.len {
return ''
}
mut start := col
mut end := col
// Find the start of the expression (scan backwards)
// Stop before the start of the line or if the character is not part of an identifier.
for start > 0 {
c := line[start - 1]
if c.is_letter() || c.is_digit() || c == `_` || c == `.` {
start--
} else {
break
}
}
// Find the end of the expression (scan forwards)
// Stop at the end of the line or if the character is not part of an identifier.
for end < line.len {
c := line[end]
if c.is_letter() || c.is_digit() || c == `_` || c == `.` {
end++
} else {
break
}
}
if start >= end {
return ''
}
return line[start..end]
}

fn (mut app App) go_to_definition(request Request) Response {
log('go_to_definition')
path := request.params.text_document.uri
// LSP line is 0-based, V compiler is 1-based
line_nr_0based := request.params.position.line
line_nr_1based := line_nr_0based + 1
col := request.params.position.char
// Get the expression under the cursor.
expression := app.get_expression_at_cursor(line_nr_0based, col)
log('found expression for definition: "${expression}"')
if expression == '' {
return Response{
id: request.id
result: 'null'
}
}
// Call the new interop function that uses the `gd^` prefix.
location := app.run_v_go_to_definition(path, line_nr_1based, expression)
log('location for definition=${location}')
// Check if the V compiler provided a definition location
if location.uri == '' {
log('no definition info found from V compiler')
return Response{
id: request.id
result: 'null'
}
}
resp := Response{
id: request.id
result: location
}
log('sending definition response: ${resp}')
return resp
}
108 changes: 36 additions & 72 deletions interop.v
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import os
import time

fn (mut app App) run_v_check(path string, text string) []JsonError {
tmpdir := os.temp_dir()
name := path.all_after_last('/')
tmppath := tmpdir + '/' + name
tmppath := os.join_path(os.temp_dir(), os.file_name(path))
log('WRITING FILE ${time.now()} ${path}')
os.write_file(tmppath, text) or { panic(err) }
log('running v.exe check')
Expand All @@ -26,82 +24,48 @@ fn (mut app App) run_v_check(path string, text string) []JsonError {
return json_errors
}

fn (mut app App) run_v_line_info(path string, line_nr int, col int) JsonVarAC {
tmpdir := os.temp_dir()
name := path.all_after_last('/')
tmppath := tmpdir + '/' + name
fn (mut app App) run_v_line_info(method Method, path string, line_info string) ResponseResult {
tmppath := os.join_path(os.temp_dir(), os.file_name(path))
log('WRITING FILE ${time.now()} ${path}')
os.write_file(tmppath, app.text) or { panic(err) }
log('running v.exe line info!')
cmd := 'v -check -json-errors -nocolor -vls-mode -line-info "${tmppath}:${line_nr}:${col}" ${tmppath}'
cmd := 'v -w -check -json-errors -nocolor -vls-mode -line-info "${tmppath}:${line_info}" ${tmppath}'
log('cmd=${cmd}')
x := os.execute(cmd)
log('RUN RES ${x}')
js := x.output
log('js=${js}')
json_errors := json.decode(JsonVarAC, x.output) or {
log('failed to parse json ${err}')
return JsonVarAC{}
}
log('json2:')
log('${json_errors}')
return json_errors
}

// In this mode V returns `/path/to/file.v:line:col`, not json
// So simply return `Location`
fn (mut app App) run_v_go_to_definition(path string, line_nr int, expr string) Location {
tmpdir := os.temp_dir()
name := path.all_after_last('/')
tmppath := tmpdir + '/' + name
log('WRITING FILE ${time.now()} ${path}')
os.write_file(tmppath, app.text) or { panic(err) }
log('running v.exe definition lookup!')
// This uses the expression instead of line/col
cmd := 'v -check -json-errors -nocolor -vls-mode -line-info "${tmppath}:${line_nr}:gd^${expr}" ${tmppath}'
log('cmd=${cmd}')
x := os.execute(cmd)
log('RUN RES ${x}')
vals := x.output.split(':')
if vals.len != 3 {
log('gotodef vals.len != 3 vals:${vals}')
return Location{}
}
line := vals[1].int()
col := vals[2].int()
return Location{
uri: 'file://' + vals[0]
range: LSPRange{
start: Position{
line: line
char: col
}
end: Position{
line: line
char: col
mut result := ResponseResult{}
match method {
.completion {
result_tmp := json.decode(JsonVarAC, x.output) or { JsonVarAC{} }
result = result_tmp.details
}
.signature_help {
result = json.decode(SignatureHelp, x.output) or { SignatureHelp{} }
}
.definition {
// file.v:line:col => Location
fields := x.output.trim_space().split(':')
if fields.len < 3 {
result = Location{}
} else {
line_nr := fields[fields.len - 2].int() - 1
col := fields[fields.len - 1].int()
result = Location{
uri: fields[..fields.len - 2].join(':')
range: LSPRange{
start: Position{
line: line_nr
char: col
}
end: Position{
line: line_nr
char: col
}
}
}
}
}
else {}
}
}

fn (mut app App) run_v_fn_sig(path string, line_nr int, char_pos int) SignatureHelp {
tmpdir := os.temp_dir()
name := path.all_after_last('/')
tmppath := tmpdir + '/' + name
log('WRITING FILE ${time.now()} ${path}')
os.write_file(tmppath, app.text) or { panic(err) }
log('running v.exe sig!')
cmd := 'v -check -json-errors -nocolor -vls-mode -line-info "${tmppath}:${line_nr}:fn^${char_pos}" ${tmppath}'
log('cmd=${cmd}')
x := os.execute(cmd)
log('RUN RES ${x}')
s := x.output
log('s=${s}')
json_errors := json.decode(SignatureHelp, x.output) or {
log('failed to parse json ${err}')
return SignatureHelp{}
}
log('json2:')
log('${json_errors}')
return json_errors
return result
}
33 changes: 4 additions & 29 deletions main.v
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,8 @@ fn (mut app App) handle_stdio_requests(mut reader io.BufferedReader) {
method := Method.from_string(request.method)
log('1method="${method}" request.method="${request.method}" kek${method == .completion}')
match method {
.completion {
log('RUNNING COMPLETION')
resp := app.completion(request)
write_response(resp)
}
.signature_help {
log('SIG HELP')
resp := app.signature_help(request)
write_response(resp)
}
.definition {
log('GO TO DEFINITION')
resp := app.go_to_definition(request)
.completion, .signature_help, .definition {
resp := app.operation_at_pos(method, request)
write_response(resp)
}
.did_change {
Expand Down Expand Up @@ -164,19 +153,7 @@ fn (mut app App) handle_stdio_requests(mut reader io.BufferedReader) {
}

fn write_response(response Response) {
mut content := json.encode(response)
// These replaces are needed because V's json encoder adds a `_type` field for union variants.
// TODO make it cleaner
content = content.replace(',"_type":"Detail"', '')
content = content.replace('"_type":"Detail",', '')
content = content.replace(',"_type":"LSPDiagnostic"', '')
content = content.replace('"_type":"LSPDiagnostic",', '')
content = content.replace(',"_type":"SignatureInformation"', '')
content = content.replace('"_type":"SignatureInformation",', '')
content = content.replace(',"_type":"ParameterInformation"', '')
content = content.replace('"_type":"ParameterInformation",', '')
content = content.replace(',"_type":"Location"', '')
content = content.replace('"_type":"Location",', '')
content := json.encode(response)
headers := $if windows {
// windows text stdio will output `\r\n` for every `\n`
'Content-Length: ${content.len}\n\n'
Expand All @@ -190,9 +167,7 @@ fn write_response(response Response) {
}

fn write_notification(notification Notification) {
mut content := json.encode(notification)
content = content.replace(',"_type":"LSPDiagnostic"', '')
content = content.replace('"_type":"LSPDiagnostic",', '')
content := json.encode(notification)
headers := $if windows {
// windows text stdio will output `\r\n` for every `\n`
'Content-Length: ${content.len}\n\n'
Expand Down