// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license that can be found in the LICENSE file. module checker import strings import v.ast import os import token enum DetailKind { text = 1 method = 2 function = 3 constructor = 4 field = 5 variable = 6 class = 7 interface = 8 module = 9 property = 10 unit = 11 value = 12 enum = 13 keyword = 14 snippet = 15 color = 16 file = 17 reference = 18 folder = 19 enum_member = 20 const = 21 struct = 22 event = 23 operator = 24 type_parameter = 25 } struct Detail { kind DetailKind // The type of item (e.g., Method, Function, Field) label string // The name of the completion item detail string // Additional info like the function signature or return type declaration string // Full fn declaration, e.g. "fn greet(name string) string" documentation string // The documentation for the item insert_text ?string insert_text_format ?int // 1 for PlainText, 2 for Snippet } fn (mut c Checker) get_fn_from_call_expr(node ast.CallExpr) !ast.Fn { fn_name := node.name return if node.is_method { left_sym := c.table.sym(c.unwrap_generic(node.left_type)) c.table.find_method(left_sym, fn_name) or { return error('failed to find method "${fn_name}"') } } else { c.table.find_fn(fn_name) or { return error('failed to find fn "${fn_name}"') } } } // Autocomplete for function parameters `os.write_bytes(**path string, bytes []u8***)` etc pub fn (mut c Checker) autocomplete_for_fn_call_expr(node ast.CallExpr) { // Hover over a function call: cursor is on the function name, method is .completion. // Output the full fn declaration as a single Detail so VLS can display it. if c.pref.linfo.method == .completion && c.vls_is_the_node(node.name_pos) { 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 // skip receiver } 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}' mut doc := '' 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 } c.vls_write_details([ Detail{ kind: .function label: fn_name declaration: declaration documentation: doc }, ]) exit(0) } if c.pref.linfo.method != .signature_help { return } if !c.vls_is_the_node(node.name_pos) { return } f := c.get_fn_from_call_expr(node) or { println(err) exit(1) } res := c.build_fn_summary(f) println(res) 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 { sym := c.table.type_symbols[idx] return sym.info.get_name_pos() } return none } fn (mut c Checker) ident_gotodef(node_ ast.Expr) { if c.pref.linfo.method != .definition { return } if !c.vls_is_the_node(node_.pos()) { return } mut node := unsafe { node_ } mut pos := token.Pos{} match mut node { ast.CallExpr { if !c.vls_is_the_node(node.name_pos) { return } f := c.get_fn_from_call_expr(node) or { println(err) exit(1) } pos = f.name_pos } ast.Ident { // global objects for _, obj in c.table.global_scope.objects { if obj is ast.ConstField && obj.name == node.name { pos = obj.pos break } else if obj is ast.GlobalField && obj.name == node.name { pos = obj.pos break } else if obj is ast.Var && obj.name == node.name { pos = obj.pos break } } // local objects if pos == token.Pos{} && !isnil(c.fn_scope) { if obj := c.fn_scope.find_var(node.name) { pos = obj.pos } } // If not found as a variable/const, try as a type if pos == token.Pos{} { full_name := if node.mod != '' { '${node.mod}.${node.name}' } else { node.name } if np := c.name_pos_gotodef(full_name) { pos = np } } } ast.StructInit { // Check if clicking on a field name in struct init 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) { pos = struct_field.pos break } } } if pos == token.Pos{} && c.vls_is_the_node(node.name_pos) { info := c.table.sym(node.typ).info if np := info.get_name_pos() { pos = np } } if pos == token.Pos{} { return } } ast.SelectorExpr { // Check if clicking on the field name or method sym := c.table.sym(node.expr_type) if field := c.table.find_field_with_embeds(sym, node.field_name) { pos = field.pos } else { if method := c.table.find_method(sym, node.field_name) { pos = method.name_pos } else { println('failed to find field or method "${node.field_name}"') exit(1) } } } ast.EnumVal { // Go to enum field definition mut enum_name := if node.enum_name == '' && node.typ != ast.void_type && node.typ != 0 { c.table.sym(node.typ).name } else { node.enum_name } if enum_decl := c.table.enum_decls[enum_name] { for field in enum_decl.fields { if field.name == node.val { pos = field.pos break } } } } ast.TypeNode { // Go to type definition typ_str := c.table.type_to_str(node.typ) if np := c.name_pos_gotodef(typ_str) { pos = np } } ast.CastExpr { // Go to type definition in cast expr if node.typname != '' { if np := c.name_pos_gotodef(node.typname) { pos = np } } if pos == token.Pos{} && node.typ != ast.void_type { typ_str := c.table.type_to_str(node.typ) if np := c.name_pos_gotodef(typ_str) { pos = np } } } else {} } if pos.file_idx != -1 { println('${c.table.filelist[pos.file_idx]}:${pos.line_nr + 1}:${pos.col}') } exit(0) } // Autocomplete for `myvar. ...`, `os. ...` fn (mut c Checker) ident_autocomplete(node ast.Ident) { if c.pref.linfo.method != .completion { return } // Mini LS hack (v -line-info "a.v:16") if c.pref.is_verbose { println( 'checker.ident_autocomplete() info.line_nr=${c.pref.linfo.line_nr} node.line_nr=${node.pos.line_nr} ' + ' node.col=${node.pos.col} pwd="${os.getwd()}" file="${c.file.path}", ' + ' pref.linfo.path="${c.pref.linfo.path}" node.name="${node.name}" node.mod="${node.mod}" col="${c.pref.linfo.col}"') } if node.mod == 'builtin' { // User can't type in `builtin.func(` at all return } if !c.vls_is_the_node(node.pos) { return } check_name := if c.pref.linfo.col == node.pos.col { if node.name == '' { // for `os` in middle of text, followed by something else in next line: // `os. // something` node.mod } else { '' } } else if c.pref.linfo.col == node.pos.col + node.pos.len { // for `os` at end of scope : // `os. }` node.name } else { '' } if check_name.len == 0 { return } // Module autocomplete // `os. ...` mod_name := c.try_resolve_to_import_mod_name(check_name) if mod_name.len > 0 { c.module_autocomplete(mod_name) exit(0) } if node.kind == .unresolved || node.obj.typ == ast.no_type { exit(0) } sym := c.table.sym(c.unwrap_generic(node.obj.typ)) mut details := []Detail{cap: 10} c.vls_gen_type_details(mut details, sym) c.vls_write_details(details) exit(0) } fn (mut c Checker) module_autocomplete(mod string) { mut details := []Detail{cap: 128} c.vls_gen_mod_funcs_details(mut details, mod) c.vls_gen_mod_type_details(mut details, mod, .alias, .interface, .enum, .sum_type, .struct) c.vls_gen_mod_consts_details(mut details, mod) c.vls_write_details(details) } fn (c &Checker) build_fn_summary(func ast.Fn) string { mut sb := strings.new_builder(128) fn_name := func.name.all_after_last('.') sb.writeln('{\n"signatures":[{') sb.write_string('\t"label":"${fn_name}(') mut params := []string{cap: func.params.len} for i, param in func.params { if func.is_method && i == 0 { // skip receiver continue } params << '${param.name} ${c.table.type_to_str(param.typ)}' } sb.write_string(params.join(', ')) sb.write_string(')') if func.return_type != ast.void_type { sb.write_string(' ') sb.write_string(c.table.type_to_str(func.return_type)) } sb.writeln('",\n\t"parameters":[{') for i, p in params { sb.write_string('\t\t"label":"${p}"') if i < params.len - 1 { sb.write_string(',') } sb.writeln('') } sb.writeln('\t}]') sb.writeln('}],') sb.writeln('"activeSignature":0,') sb.writeln('"activeParameter":0') sb.writeln('}') return sb.str() } fn (c &Checker) try_resolve_to_import_mod_name(name string) string { for imp in c.file.imports { if (imp.alias == name || imp.mod == name) && imp.alias in c.file.used_imports { return imp.mod } } return '' } fn (c &Checker) vls_gen_mod_funcs_details(mut details []Detail, mod string) { for _, f in c.table.fns { mut name := f.name if f.is_pub && !f.is_method && !f.is_static_type_method && name.all_before_last('.') == mod { name = name.all_after_last('.') // The user already typed `mod.`, so suggest the name without module type_string := if f.return_type != ast.no_type { c.table.type_to_str(f.return_type) } else { '' } mut doc := '' if info := c.table.vls_info['fn_${mod}[]${name}'] { doc = info.doc } // Build full fn declaration for hover display, e.g. "fn add(a int, b int) int" mut params := []string{cap: f.params.len} for param in f.params { 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 ${name}(${params.join(', ')})${ret_str}' details << Detail{ kind: .function label: name detail: type_string declaration: declaration documentation: doc } } } } fn (c &Checker) vls_gen_mod_type_details(mut details []Detail, mod string, kinds ...ast.Kind) { for sym in c.table.type_symbols { if sym.is_pub && sym.kind in kinds { if sym.name.all_before_last('.') == mod { mut doc := '' key := match sym.kind { .alias { 'aliastype' } .interface { 'interface' } .enum { 'enum' } .sum_type { 'sumtype' } .struct { 'struct' } else { '${sym.kind}' } } if info := c.table.vls_info['${key}_${sym.name}'] { doc = info.doc } details << Detail{ kind: c.vls_map_v_kind_to_lsp_kind(sym.kind) label: sym.name.all_after_last('.') documentation: doc } } } } } fn (c &Checker) vls_gen_mod_consts_details(mut details []Detail, mod string) { for _, obj in c.table.global_scope.objects { if obj is ast.ConstField && obj.is_pub { if obj.name.all_before_last('.') == mod { mut doc := '' if info := c.table.vls_info['const_${obj.name}'] { doc = info.doc } details << Detail{ kind: .const label: obj.name.all_after_last('.') documentation: doc } } } } } fn (c &Checker) vls_gen_type_details(mut details []Detail, sym ast.TypeSymbol) { match sym.kind { .struct { struct_info := sym.info as ast.Struct for field in struct_info.fields { field_sym := c.table.sym(field.typ) details << Detail{ kind: .field label: field.name detail: field_sym.name } } } .array { if sym.info is ast.Aggregate { } else if sym.info is ast.Array { details << Detail{ kind: .property label: 'len' detail: 'int' } details << Detail{ kind: .property // use class icon label: 'cap' detail: 'int' } } } .string { details << Detail{ kind: .property // use class icon label: 'len' detail: 'int' } } else {} } // Aliases and other types can have methods, add them for method in sym.methods { method_ret_type := c.table.sym(method.return_type) details << Detail{ kind: .method label: method.name detail: method_ret_type.name } } } fn (c &Checker) vls_write_details(details []Detail) { mut sb := strings.new_builder(details.len * 32) sb.writeln('{"details": [') for detail in details { sb.write_string('{"kind":${int(detail.kind)},') sb.write_string('"label":"${detail.label}",') sb.write_string('"detail":"${detail.detail}",') sb.write_string('"declaration":"${detail.declaration}",') sb.write_string('"documentation":"${detail.documentation}",') if insert_text := detail.insert_text { sb.write_string('"insert_text":"${insert_text}",') } if insert_text_format := detail.insert_text_format { sb.write_string('"insert_text_format":${insert_text_format},') } sb.go_back(1) sb.writeln('},') } if details.len > 0 { sb.go_back(2) } sb.write_string('\n]}') print(sb.str()) } fn (c &Checker) vls_map_v_kind_to_lsp_kind(kind ast.Kind) DetailKind { match kind { .alias, .sum_type { return .class } .function { return .function } .interface { return .interface } .enum { return .enum } .struct { return .struct } else { return .text } } return .text } fn (c &Checker) vls_is_the_node(pos token.Pos) bool { // Make sure this ident is on the same line and same file as request same_line := c.pref.linfo.line_nr == pos.line_nr if !same_line { return false } if pos.file_idx < 0 { return false } // Normalize paths for comparison to handle directory compilation linfo_path := os.real_path(c.pref.linfo.path) file_path := os.real_path(c.table.filelist[pos.file_idx]) if linfo_path != file_path { return false } if c.pref.linfo.col > pos.col + pos.len || c.pref.linfo.col < pos.col { return false } return true }