From 2f8717723fcf2ffae2120ba249c9b6f3b37a2f47 Mon Sep 17 00:00:00 2001 From: CreeperFace <165158232+dy-tea@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:46:27 +0100 Subject: [PATCH] checker,pref: implement hover method for vls (#26821) * checker,pref: implement hover method for vls * review --- vlib/v/builder/builder.v | 3 +- vlib/v/checker/autocomplete.v | 215 ++++++++++++++++++++ vlib/v/checker/checker.v | 1 + vlib/v/pref/line_info.v | 4 + vlib/v/tests/vls/autocomplete_module_test.v | 39 ++++ 5 files changed, 261 insertions(+), 1 deletion(-) diff --git a/vlib/v/builder/builder.v b/vlib/v/builder/builder.v index fc9bc84ea..50a0eae3a 100644 --- a/vlib/v/builder/builder.v +++ b/vlib/v/builder/builder.v @@ -577,7 +577,8 @@ pub fn (mut b Builder) print_warnings_and_errors() { } } if b.pref.json_errors { - if !b.pref.is_vls || b.pref.linfo.method !in [.definition, .completion, .signature_help] { + if !b.pref.is_vls + || b.pref.linfo.method !in [.definition, .completion, .signature_help, .hover] { util.print_json_errors(json_errors) } // eprintln(json2.encode_pretty(json_errors)) diff --git a/vlib/v/checker/autocomplete.v b/vlib/v/checker/autocomplete.v index d1d5c6336..485d080dd 100644 --- a/vlib/v/checker/autocomplete.v +++ b/vlib/v/checker/autocomplete.v @@ -111,6 +111,221 @@ pub fn (mut c Checker) autocomplete_for_fn_call_expr(node ast.CallExpr) { exit(0) } +fn (mut c Checker) ident_hover(node_ ast.Expr) { + if c.pref.linfo.method != .hover { + return + } + if !c.vls_is_the_node(node_.pos()) { + return + } + mut node := unsafe { node_ } + mut declaration := '' + mut doc := '' + match mut node { + ast.CallExpr { + if !c.vls_is_the_node(node.name_pos) { + return + } + f := c.get_fn_from_call_expr(node) or { return } + fn_name := f.name.all_after_last('.') + mut params := []string{cap: f.params.len} + for i, param in f.params { + if f.is_method && i == 0 { + continue + } + params << '${param.name} ${c.table.type_to_str(param.typ)}' + } + ret_str := if f.return_type != ast.no_type && f.return_type != ast.void_type { + ' ' + c.table.type_to_str(f.return_type) + } else { + '' + } + declaration = 'fn ${fn_name}(${params.join(', ')})${ret_str}' + receiver := if f.is_method { + c.table.sym(f.receiver_type).name.all_after_last('.') + } else { + '' + } + if info := c.table.vls_info['fn_${f.mod}[${receiver}]${fn_name}'] { + doc = info.doc + } + } + ast.Ident { + for _, obj in c.table.global_scope.objects { + if obj is ast.ConstField && obj.name == node.name { + type_str := c.table.type_to_str(obj.typ) + declaration = 'const ${node.name.all_after_last('.')} ${type_str}' + if info := c.table.vls_info['const_${obj.name}'] { + doc = info.doc + } + break + } else if obj is ast.GlobalField && obj.name == node.name { + type_str := c.table.type_to_str(obj.typ) + declaration = '__global ${node.name.all_after_last('.')} ${type_str}' + break + } + } + if declaration == '' && !isnil(c.fn_scope) { + if obj := c.fn_scope.find_var(node.name) { + type_str := c.table.type_to_str(obj.typ) + declaration = '${node.name} ${type_str}' + } + } + if declaration == '' { + full_name := if node.mod != '' { '${node.mod}.${node.name}' } else { node.name } + idx := c.table.find_type_idx(full_name) + if idx > 0 { + sym := c.table.type_symbols[idx] + declaration = c.vls_hover_type_declaration(sym) + key := c.vls_hover_type_info_key(sym) + if info := c.table.vls_info['${key}_${sym.name}'] { + doc = info.doc + } + } + } + } + ast.SelectorExpr { + sym := c.table.sym(node.expr_type) + if field := c.table.find_field_with_embeds(sym, node.field_name) { + type_str := c.table.type_to_str(field.typ) + declaration = '${node.field_name} ${type_str}' + } else if method := c.table.find_method(sym, node.field_name) { + fn_name := method.name.all_after_last('.') + mut params := []string{cap: method.params.len} + for i, param in method.params { + if method.is_method && i == 0 { + continue + } + params << '${param.name} ${c.table.type_to_str(param.typ)}' + } + ret_str := if method.return_type != ast.no_type + && method.return_type != ast.void_type { + ' ' + c.table.type_to_str(method.return_type) + } else { + '' + } + declaration = 'fn ${fn_name}(${params.join(', ')})${ret_str}' + receiver_name := sym.name.all_after_last('.') + if info := c.table.vls_info['fn_${method.mod}[${receiver_name}]${fn_name}'] { + doc = info.doc + } + } else { + return + } + } + ast.StructInit { + if c.vls_is_the_node(node.name_pos) { + sym := c.table.sym(node.typ) + declaration = c.vls_hover_type_declaration(sym) + key := c.vls_hover_type_info_key(sym) + if info := c.table.vls_info['${key}_${sym.name}'] { + doc = info.doc + } + } else { + for field in node.init_fields { + if c.vls_is_the_node(field.name_pos) { + sym := c.table.sym(node.typ) + if struct_field := c.table.find_field_with_embeds(sym, field.name) { + type_str := c.table.type_to_str(struct_field.typ) + declaration = '${field.name} ${type_str}' + } + break + } + } + } + } + ast.EnumVal { + enum_name := if node.enum_name == '' && node.typ != ast.void_type && node.typ != 0 { + c.table.sym(node.typ).name + } else { + node.enum_name + } + declaration = '${enum_name.all_after_last('.')}.${node.val}' + } + ast.TypeNode { + typ_str := c.table.type_to_str(node.typ) + idx := c.table.find_type_idx(typ_str) + if idx > 0 { + sym := c.table.type_symbols[idx] + declaration = c.vls_hover_type_declaration(sym) + key := c.vls_hover_type_info_key(sym) + if info := c.table.vls_info['${key}_${sym.name}'] { + doc = info.doc + } + } + } + ast.CastExpr { + typ_str := if node.typname != '' { + node.typname + } else { + c.table.type_to_str(node.typ) + } + idx := c.table.find_type_idx(typ_str) + if idx > 0 { + sym := c.table.type_symbols[idx] + declaration = c.vls_hover_type_declaration(sym) + key := c.vls_hover_type_info_key(sym) + if info := c.table.vls_info['${key}_${sym.name}'] { + doc = info.doc + } + } + } + else {} + } + if declaration == '' { + exit(0) + } + c.vls_write_hover(declaration, doc) + exit(0) +} + +fn (c &Checker) vls_hover_type_declaration(sym ast.TypeSymbol) string { + name := sym.name.all_after_last('.') + match sym.kind { + .struct { + return 'struct ${name}' + } + .enum { + return 'enum ${name}' + } + .interface { + return 'interface ${name}' + } + .alias { + alias_info := sym.info as ast.Alias + parent_str := c.table.type_to_str(alias_info.parent_type) + return 'type ${name} = ${parent_str}' + } + .sum_type { + sum_info := sym.info as ast.SumType + variants := sum_info.variants.map(c.table.type_to_str(it)).join(' | ') + return 'type ${name} = ${variants}' + } + else { + return name + } + } +} + +fn (c &Checker) vls_hover_type_info_key(sym ast.TypeSymbol) string { + return match sym.kind { + .alias { 'aliastype' } + .sum_type { 'sumtype' } + else { '${sym.kind}' } + } +} + +fn (c &Checker) vls_write_hover(declaration string, doc string) { + mut lines := ['```v', declaration, '```'] + if doc.len > 0 { + lines << '' + lines << doc + } + value := lines.join('\n').replace('\\', '\\\\').replace('"', '\\"').replace('\n', + '\\n') + println('{"contents":{"kind":"markdown","value":"${value}"}}') +} + fn (mut c Checker) name_pos_gotodef(name string) ?token.Pos { idx := c.table.find_type_idx(name) if idx > 0 { diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 2fa2b356e..60ca6b771 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -3984,6 +3984,7 @@ pub fn (mut c Checker) expr(mut node ast.Expr) ast.Type { defer { if c.pref.is_vls { c.ident_gotodef(node) + c.ident_hover(node) } } match mut node { diff --git a/vlib/v/pref/line_info.v b/vlib/v/pref/line_info.v index fa152d3aa..d1fb5ee2d 100644 --- a/vlib/v/pref/line_info.v +++ b/vlib/v/pref/line_info.v @@ -12,6 +12,7 @@ pub enum Method { definition @['textDocument/definition'] completion @['textDocument/completion'] signature_help @['textDocument/signatureHelp'] + hover @['textDocument/hover'] set_trace @['$/setTrace'] cancel_request @['$/cancelRequest'] shutdown @['shutdown'] @@ -51,6 +52,9 @@ fn (mut p Preferences) parse_line_info(line string) { } else if third.starts_with('gd^') { col = third[3..].int() - 1 Method.definition + } else if third.starts_with('hv^') { + col = third[3..].int() - 1 + Method.hover } else if third[0].is_digit() { col = third.int() - 1 Method.completion diff --git a/vlib/v/tests/vls/autocomplete_module_test.v b/vlib/v/tests/vls/autocomplete_module_test.v index 1c7438c7c..5aaffaf9f 100644 --- a/vlib/v/tests/vls/autocomplete_module_test.v +++ b/vlib/v/tests/vls/autocomplete_module_test.v @@ -36,6 +36,10 @@ const autocomplete_info_for_mod_struct = '{"details": [ {"kind":2,"label":"add","detail":"void","declaration":"","documentation":""} ]}' +const hover_info_for_public_fn1 = '{"contents":{"kind":"markdown","value":"```v\\nfn public_fn1(val int) string\\n```"}}' + +const hover_info_for_public_struct1 = '{"contents":{"kind":"markdown","value":"```v\\nstruct PublicStruct1\\n```"}}' + const fn_signature_info_for_all_before_last = '{ "signatures":[{ "label":"all_before_last(sub string) string", @@ -57,6 +61,7 @@ enum Method { definition @['textDocument/definition'] completion @['textDocument/completion'] signature_help @['textDocument/signatureHelp'] + hover @['textDocument/hover'] set_trace @['$/setTrace'] cancel_request @['$/cancelRequest'] shutdown @['shutdown'] @@ -110,6 +115,16 @@ const test_data = [ cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:28:9" ${os.quoted_path(text_file)}' output: '' }, + TestData{ + method: .hover + cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:30:hv^10" ${os.quoted_path(text_file)}' + output: hover_info_for_public_fn1 + }, + TestData{ + method: .hover + cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:31:hv^12" ${os.quoted_path(text_file)}' + output: hover_info_for_public_struct1 + }, TestData{ method: .definition cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:30:gd^10" ${os.quoted_path(text_file)}' @@ -352,6 +367,9 @@ fn test_main() { .signature_help { check_valid_fn_signature(t.output)! } + .hover { + check_valid_hover(t.output)! + } else {} } } @@ -410,6 +428,27 @@ fn check_valid_json_errors(message string) ! { } } +struct HoverContents { + kind string + value string +} + +struct HoverResult { + contents HoverContents +} + +fn check_valid_hover(message string) ! { + result := json.decode(HoverResult, message) or { + return error('hover: fail to json decode: ${err}') + } + if result.contents.kind != 'markdown' { + return error('hover: contents.kind should be "markdown": ${result.contents.kind}') + } + if result.contents.value.len == 0 { + return error('hover: contents.value should not be empty') + } +} + fn check_valid_fn_signature(message string) ! { result := json.decode(SignatureHelp, message) or { return error('fn_signature: fail to json decode') -- 2.39.5