diff options
| -rw-r--r-- | src/server/collector.odin | 281 | ||||
| -rw-r--r-- | src/server/methods.odin | 296 | ||||
| -rw-r--r-- | src/testing/testing.odin | 47 | ||||
| -rw-r--r-- | tests/completions_test.odin | 173 |
4 files changed, 672 insertions, 125 deletions
diff --git a/src/server/collector.odin b/src/server/collector.odin index d8a4e65..37d3cd8 100644 --- a/src/server/collector.odin +++ b/src/server/collector.odin @@ -32,10 +32,11 @@ Method :: struct { } SymbolPackage :: struct { - symbols: map[string]Symbol, - objc_structs: map[string]ObjcStruct, //mapping from struct name to function - methods: map[Method][dynamic]Symbol, - imports: [dynamic]string, //Used for references to figure whether the package is even able to reference the symbol + symbols: map[string]Symbol, + objc_structs: map[string]ObjcStruct, //mapping from struct name to function + methods: map[Method][dynamic]Symbol, + imports: [dynamic]string, //Used for references to figure whether the package is even able to reference the symbol + proc_group_members: map[string]bool, // Tracks procedure names that are part of proc groups (used by fake methods) } get_index_unique_string :: proc { @@ -437,46 +438,183 @@ add_comp_lit_fields :: proc( generic.ranges = ranges[:] } +/* + Records the names of procedures that are part of a proc group. + This is used by the fake methods feature to hide individual procs + when the proc group should be shown instead. +*/ +record_proc_group_members :: proc(collection: ^SymbolCollection, group: ^ast.Proc_Group, pkg_name: string) { + pkg := get_or_create_package(collection, pkg_name) + + for arg in group.args { + name := get_proc_group_member_name(arg) or_continue + pkg.proc_group_members[get_index_unique_string(collection, name)] = true + } +} + +@(private = "file") +get_proc_group_member_name :: proc(expr: ^ast.Expr) -> (name: string, ok: bool) { + #partial switch v in expr.derived { + case ^ast.Ident: + return v.name, true + case ^ast.Selector_Expr: + // For package.proc_name, we only care about the proc name + if field, is_ident := v.field.derived.(^ast.Ident); is_ident { + return field.name, true + } + } + return "", false +} + +@(private = "file") +get_or_create_package :: proc(collection: ^SymbolCollection, pkg_name: string) -> ^SymbolPackage { + pkg := &collection.packages[pkg_name] + if pkg.symbols == nil { + collection.packages[pkg_name] = {} + pkg = &collection.packages[pkg_name] + pkg.symbols = make(map[string]Symbol, 100, collection.allocator) + pkg.methods = make(map[Method][dynamic]Symbol, 100, collection.allocator) + pkg.objc_structs = make(map[string]ObjcStruct, 5, collection.allocator) + pkg.proc_group_members = make(map[string]bool, 10, collection.allocator) + } + return pkg +} + +/* + Collects a procedure as a fake method if it's not part of a proc group. +*/ collect_method :: proc(collection: ^SymbolCollection, symbol: Symbol) { pkg := &collection.packages[symbol.pkg] - if value, ok := symbol.value.(SymbolProcedureValue); ok { - if len(value.arg_types) == 0 { - return + if symbol.name in pkg.proc_group_members { + return + } + + value, ok := symbol.value.(SymbolProcedureValue) + if !ok { + return + } + if len(value.arg_types) == 0 { + return + } + + method, method_ok := get_method_from_first_arg(collection, value.arg_types[0].type, symbol.pkg) + if !method_ok { + return + } + add_symbol_to_method(collection, pkg, method, symbol) +} + +/* + Collects a proc group as a fake method based on its member procedures' first arguments. + The proc group is registered as a method for each distinct first-argument type + across all its members. +*/ +collect_proc_group_method :: proc(collection: ^SymbolCollection, symbol: Symbol) { + pkg := &collection.packages[symbol.pkg] + + group_value, ok := symbol.value.(SymbolProcedureGroupValue) + if !ok { + return + } + + proc_group, is_proc_group := group_value.group.derived.(^ast.Proc_Group) + if !is_proc_group || len(proc_group.args) == 0 { + return + } + + // Track which method keys we've already registered to avoid duplicates + registered_methods := make(map[Method]bool, len(proc_group.args), context.temp_allocator) + + // Register the proc group as a method for each distinct first-argument type + for member_expr in proc_group.args { + member_name, name_ok := get_proc_group_member_name(member_expr) + if !name_ok { + continue } - expr, _, ok := unwrap_pointer_ident(value.arg_types[0].type) + member_symbol, found := pkg.symbols[member_name] + if !found { + continue + } - if !ok { - return + member_proc, is_proc := member_symbol.value.(SymbolProcedureValue) + if !is_proc || len(member_proc.arg_types) == 0 { + continue } - method: Method + method, method_ok := get_method_from_first_arg(collection, member_proc.arg_types[0].type, symbol.pkg) + if !method_ok { + continue + } - #partial switch v in expr.derived { - case ^ast.Selector_Expr: - if ident, ok := v.expr.derived.(^ast.Ident); ok { - method.pkg = get_index_unique_string(collection, ident.name) - method.name = get_index_unique_string(collection, v.field.name) - } else { - return - } - case ^ast.Ident: - method.pkg = symbol.pkg - method.name = get_index_unique_string(collection, v.name) - case: - return + if method not_in registered_methods { + registered_methods[method] = true + add_symbol_to_method(collection, pkg, method, symbol) } + } +} - symbols := &pkg.methods[method] +@(private = "file") +get_method_from_first_arg :: proc( + collection: ^SymbolCollection, + first_arg_type: ^ast.Expr, + default_pkg: string, +) -> ( + method: Method, + ok: bool, +) { + expr, _, unwrap_ok := unwrap_pointer_ident(first_arg_type) + if !unwrap_ok { + return {}, false + } - if symbols == nil { - pkg.methods[method] = make([dynamic]Symbol, collection.allocator) - symbols = &pkg.methods[method] + #partial switch v in expr.derived { + case ^ast.Selector_Expr: + ident, is_ident := v.expr.derived.(^ast.Ident) + if !is_ident { + return {}, false } + method.pkg = get_index_unique_string(collection, ident.name) + method.name = get_index_unique_string(collection, v.field.name) + case ^ast.Ident: + if is_builtin_type_name(v.name) { + method.pkg = "$builtin" + } else { + method.pkg = default_pkg + } + method.name = get_index_unique_string(collection, v.name) + case: + return {}, false + } - append(symbols, symbol) + return method, true +} + +is_builtin_type_name :: proc(name: string) -> bool { + for names in untyped_map { + for builtin_name in names { + if name == builtin_name { + return true + } + } + } + // Also check some other builtin types not in untyped_map + switch name { + case "rawptr", "uintptr", "typeid", "any", "rune": + return true + } + return false +} + +@(private = "file") +add_symbol_to_method :: proc(collection: ^SymbolCollection, pkg: ^SymbolPackage, method: Method, symbol: Symbol) { + symbols := &pkg.methods[method] + if symbols == nil { + pkg.methods[method] = make([dynamic]Symbol, collection.allocator) + symbols = &pkg.methods[method] } + append(symbols, symbol) } collect_objc :: proc(collection: ^SymbolCollection, attributes: []^ast.Attribute, symbol: Symbol) { @@ -554,6 +692,20 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri } } + // Compute pkg early so it's available inside the switch + if expr.builtin || strings.contains(uri, "builtin.odin") { + symbol.pkg = "$builtin" + } else if strings.contains(uri, "intrinsics.odin") { + intrinsics_path := filepath.join( + elems = {common.config.collections["base"], "/intrinsics"}, + allocator = context.temp_allocator, + ) + intrinsics_path, _ = filepath.to_slash(intrinsics_path, context.temp_allocator) + symbol.pkg = get_index_unique_string(collection, intrinsics_path) + } else { + symbol.pkg = get_index_unique_string(collection, directory) + } + #partial switch v in col_expr.derived { case ^ast.Matrix_Type: token = v^ @@ -601,6 +753,10 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri symbol.value = SymbolProcedureGroupValue { group = clone_type(col_expr, collection.allocator, &collection.unique_strings), } + // Record proc group members for fake methods feature + if collection.config != nil && collection.config.enable_fake_method { + record_proc_group_members(collection, v, symbol.pkg) + } case ^ast.Struct_Type: token = v^ token_type = .Struct @@ -712,20 +868,7 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri comment, _ := get_file_comment(file, symbol.range.start.line + 1) symbol.comment = get_comment(comment, collection.allocator) - if expr.builtin || strings.contains(uri, "builtin.odin") { - symbol.pkg = "$builtin" - } else if strings.contains(uri, "intrinsics.odin") { - path := filepath.join( - elems = {common.config.collections["base"], "/intrinsics"}, - allocator = context.temp_allocator, - ) - - path, _ = filepath.to_slash(path, context.temp_allocator) - - symbol.pkg = get_index_unique_string(collection, path) - } else { - symbol.pkg = get_index_unique_string(collection, directory) - } + // symbol.pkg was already set earlier before the switch if is_distinct { symbol.flags |= {.Distinct} @@ -764,16 +907,13 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri pkg.symbols = make(map[string]Symbol, 100, collection.allocator) pkg.methods = make(map[Method][dynamic]Symbol, 100, collection.allocator) pkg.objc_structs = make(map[string]ObjcStruct, 5, collection.allocator) + pkg.proc_group_members = make(map[string]bool, 10, collection.allocator) } if .ObjC in symbol.flags { collect_objc(collection, expr.attributes, symbol) } - if symbol.type == .Function && common.config.enable_fake_method { - collect_method(collection, symbol) - } - if v, ok := pkg.symbols[symbol.name]; !ok || v.name == "" { pkg.symbols[symbol.name] = symbol } else { @@ -781,12 +921,59 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri } } + // Second pass: collect fake methods after all symbols and proc group members are recorded + if collection.config != nil && collection.config.enable_fake_method { + collect_fake_methods(collection, exprs, directory, uri) + } + collect_imports(collection, file, directory) return .None } +/* + Collects fake methods for all procedures and proc groups. + This is done as a second pass after all symbols are collected, + so that we know which procedures are part of proc groups. +*/ +@(private = "file") +collect_fake_methods :: proc(collection: ^SymbolCollection, exprs: []GlobalExpr, directory: string, uri: string) { + for expr in exprs { + // Determine the package name (same logic as in collect_symbols) + pkg_name: string + if expr.builtin || strings.contains(uri, "builtin.odin") { + pkg_name = "$builtin" + } else if strings.contains(uri, "intrinsics.odin") { + intrinsics_path := filepath.join( + elems = {common.config.collections["base"], "/intrinsics"}, + allocator = context.temp_allocator, + ) + intrinsics_path, _ = filepath.to_slash(intrinsics_path, context.temp_allocator) + pkg_name = get_index_unique_string(collection, intrinsics_path) + } else { + pkg_name = get_index_unique_string(collection, directory) + } + + pkg, ok := &collection.packages[pkg_name] + if !ok { + continue + } + + symbol, found := pkg.symbols[expr.name] + if !found { + continue + } + + #partial switch _ in symbol.value { + case SymbolProcedureValue: + collect_method(collection, symbol) + case SymbolProcedureGroupValue: + collect_proc_group_method(collection, symbol) + } + } +} + Reference :: struct { identifiers: [dynamic]common.Location, selectors: map[string][dynamic]common.Range, diff --git a/src/server/methods.odin b/src/server/methods.odin index 757b37d..19d9ff4 100644 --- a/src/server/methods.odin +++ b/src/server/methods.odin @@ -73,7 +73,7 @@ append_method_completion :: proc( for c in cases { method := Method { name = c, - pkg = selector_symbol.pkg, + pkg = "$builtin", // Untyped values are always builtin types } collect_methods( ast_context, @@ -86,9 +86,14 @@ append_method_completion :: proc( ) } } else { + // For typed values, check if it's a builtin type + method_pkg := selector_symbol.pkg + if is_builtin_type_name(selector_symbol.name) { + method_pkg = "$builtin" + } method := Method { name = selector_symbol.name, - pkg = selector_symbol.pkg, + pkg = method_pkg, } collect_methods( ast_context, @@ -114,83 +119,218 @@ collect_methods :: proc( results: ^[dynamic]CompletionResult, ) { for k, v in indexer.index.collection.packages { - if symbols, ok := &v.methods[method]; ok { - for &symbol in symbols { - if should_skip_private_symbol(symbol, ast_context.current_package, ast_context.fullpath) { - continue - } - resolve_unresolved_symbol(ast_context, &symbol) - - range, ok := get_range_from_selection_start_to_dot(position_context) - - if !ok { - return - } - - value: SymbolProcedureValue - value, ok = symbol.value.(SymbolProcedureValue) - - if !ok { - continue - } - - if len(value.arg_types) == 0 || value.arg_types[0].type == nil { - continue - } - - first_arg: Symbol - first_arg, ok = resolve_type_expression(ast_context, value.arg_types[0].type) - - if !ok { - continue - } - - pointers_to_add := first_arg.pointers - pointers - - references := "" - dereferences := "" - - if pointers_to_add > 0 { - for i in 0 ..< pointers_to_add { - references = fmt.tprintf("%v&", references) - } - } else if pointers_to_add < 0 { - for i in pointers_to_add ..< 0 { - dereferences = fmt.tprintf("%v^", dereferences) - } - } - - new_text := "" - - if symbol.pkg != ast_context.document_package { - new_text = fmt.tprintf( - "%v.%v", - path.base(get_symbol_pkg_name(ast_context, &symbol), false, ast_context.allocator), - symbol.name, - ) - } else { - new_text = fmt.tprintf("%v", symbol.name) - } - - if len(symbol.value.(SymbolProcedureValue).arg_types) > 1 { - new_text = fmt.tprintf("%v(%v%v%v$0)", new_text, references, receiver, dereferences) - } else { - new_text = fmt.tprintf("%v(%v%v%v)$0", new_text, references, receiver, dereferences) - } - - item := CompletionItem { - label = symbol.name, - kind = symbol_type_to_completion_kind(symbol.type), - detail = get_short_signature(ast_context, symbol), - additionalTextEdits = remove_edit, - textEdit = TextEdit{newText = new_text, range = {start = range.end, end = range.end}}, - insertTextFormat = .Snippet, - InsertTextMode = .adjustIndentation, - documentation = construct_symbol_docs(symbol), - } - - append(results, CompletionResult{completion_item = item}) + symbols, ok := &v.methods[method] + if !ok { + continue + } + + for &symbol in symbols { + if should_skip_private_symbol(symbol, ast_context.current_package, ast_context.fullpath) { + continue + } + resolve_unresolved_symbol(ast_context, &symbol) + + #partial switch &sym_value in symbol.value { + case SymbolProcedureValue: + add_proc_method_completion( + ast_context, + position_context, + &symbol, + sym_value, + pointers, + receiver, + remove_edit, + results, + ) + case SymbolProcedureGroupValue: + add_proc_group_method_completion( + ast_context, + position_context, + &symbol, + sym_value, + pointers, + receiver, + remove_edit, + results, + ) + } + } + } +} + +@(private = "file") +add_proc_method_completion :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + symbol: ^Symbol, + value: SymbolProcedureValue, + pointers: int, + receiver: string, + remove_edit: []TextEdit, + results: ^[dynamic]CompletionResult, +) { + if len(value.arg_types) == 0 || value.arg_types[0].type == nil { + return + } + + range, ok := get_range_from_selection_start_to_dot(position_context) + if !ok { + return + } + + first_arg: Symbol + first_arg, ok = resolve_type_expression(ast_context, value.arg_types[0].type) + if !ok { + return + } + + references, dereferences := compute_pointer_adjustments(first_arg.pointers, pointers) + + new_text := build_method_call_text( + ast_context, + symbol, + receiver, + references, + dereferences, + len(value.arg_types) > 1, + ) + + item := CompletionItem { + label = symbol.name, + kind = symbol_type_to_completion_kind(symbol.type), + detail = get_short_signature(ast_context, symbol^), + additionalTextEdits = remove_edit, + textEdit = TextEdit{newText = new_text, range = {start = range.end, end = range.end}}, + insertTextFormat = .Snippet, + InsertTextMode = .adjustIndentation, + documentation = construct_symbol_docs(symbol^), + } + + append(results, CompletionResult{completion_item = item}) +} + +@(private = "file") +add_proc_group_method_completion :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + symbol: ^Symbol, + value: SymbolProcedureGroupValue, + pointers: int, + receiver: string, + remove_edit: []TextEdit, + results: ^[dynamic]CompletionResult, +) { + proc_group, is_group := value.group.derived.(^ast.Proc_Group) + if !is_group || len(proc_group.args) == 0 { + return + } + + range, ok := get_range_from_selection_start_to_dot(position_context) + if !ok { + return + } + + // Get first member to determine pointer adjustments + first_member: Symbol + first_member, ok = resolve_type_expression(ast_context, proc_group.args[0]) + if !ok { + return + } + + member_proc, is_proc := first_member.value.(SymbolProcedureValue) + if !is_proc || len(member_proc.arg_types) == 0 || member_proc.arg_types[0].type == nil { + return + } + + first_arg: Symbol + first_arg, ok = resolve_type_expression(ast_context, member_proc.arg_types[0].type) + if !ok { + return + } + + references, dereferences := compute_pointer_adjustments(first_arg.pointers, pointers) + + // Check if any member of the proc group has additional arguments beyond the receiver + has_additional_args := false + for member_expr in proc_group.args { + member: Symbol + member, ok = resolve_type_expression(ast_context, member_expr) + if !ok { + continue + } + if proc_val, is_proc_val := member.value.(SymbolProcedureValue); is_proc_val { + if len(proc_val.arg_types) > 1 { + has_additional_args = true + break } } } + + new_text := build_method_call_text(ast_context, symbol, receiver, references, dereferences, has_additional_args) + + item := CompletionItem { + label = symbol.name, + kind = symbol_type_to_completion_kind(symbol.type), + detail = get_short_signature(ast_context, symbol^), + additionalTextEdits = remove_edit, + textEdit = TextEdit{newText = new_text, range = {start = range.end, end = range.end}}, + insertTextFormat = .Snippet, + InsertTextMode = .adjustIndentation, + documentation = construct_symbol_docs(symbol^), + } + + append(results, CompletionResult{completion_item = item}) +} + +@(private = "file") +compute_pointer_adjustments :: proc( + first_arg_pointers: int, + current_pointers: int, +) -> ( + references: string, + dereferences: string, +) { + pointers_to_add := first_arg_pointers - current_pointers + + if pointers_to_add > 0 { + for _ in 0 ..< pointers_to_add { + references = fmt.tprintf("%v&", references) + } + } else if pointers_to_add < 0 { + for _ in pointers_to_add ..< 0 { + dereferences = fmt.tprintf("%v^", dereferences) + } + } + + return references, dereferences +} + +@(private = "file") +build_method_call_text :: proc( + ast_context: ^AstContext, + symbol: ^Symbol, + receiver: string, + references: string, + dereferences: string, + has_additional_args: bool, +) -> string { + new_text: string + + if symbol.pkg != ast_context.document_package { + new_text = fmt.tprintf( + "%v.%v", + path.base(get_symbol_pkg_name(ast_context, symbol), false, ast_context.allocator), + symbol.name, + ) + } else { + new_text = fmt.tprintf("%v", symbol.name) + } + + if has_additional_args { + new_text = fmt.tprintf("%v(%v%v%v$0)", new_text, references, receiver, dereferences) + } else { + new_text = fmt.tprintf("%v(%v%v%v)$0", new_text, references, receiver, dereferences) + } + + return new_text } diff --git a/src/testing/testing.odin b/src/testing/testing.odin index 2013f79..0ba7b0f 100644 --- a/src/testing/testing.odin +++ b/src/testing/testing.odin @@ -68,6 +68,9 @@ setup :: proc(src: ^Source) { server.setup_index() + // Set the collection's config to the test's config to enable feature flags like enable_fake_method + server.indexer.index.collection.config = &src.config + server.document_setup(src.document) server.document_refresh(src.document, &src.config, nil) @@ -318,6 +321,50 @@ expect_completion_insert_text :: proc( } } +expect_completion_edit_text :: proc( + t: ^testing.T, + src: ^Source, + trigger_character: string, + label: string, + expected_text: string, +) { + setup(src) + defer teardown(src) + + completion_context := server.CompletionContext { + triggerCharacter = trigger_character, + } + + completion_list, ok := server.get_completion_list(src.document, src.position, completion_context, &src.config) + + if !ok { + log.error("Failed get_completion_list") + } + + found := false + for completion in completion_list.items { + if completion.label == label { + found = true + if text_edit, has_edit := completion.textEdit.(server.TextEdit); has_edit { + if text_edit.newText != expected_text { + log.errorf( + "Completion '%v' expected textEdit.newText %q, but received %q", + label, + expected_text, + text_edit.newText, + ) + } + } else { + log.errorf("Completion '%v' has no textEdit", label) + } + break + } + } + if !found { + log.errorf("Expected completion label '%v' not found in %v", label, completion_list.items) + } +} + expect_hover :: proc(t: ^testing.T, src: ^Source, expect_hover_string: string) { setup(src) defer teardown(src) diff --git a/tests/completions_test.odin b/tests/completions_test.odin index 19f5363..56553c3 100644 --- a/tests/completions_test.odin +++ b/tests/completions_test.odin @@ -5327,3 +5327,176 @@ ast_completion_parapoly_struct_with_parapoly_child :: proc(t: ^testing.T) { } test.expect_completion_docs(t, &source, "", {"ChildStruct.GenericParam: test.SomeEnum", "ChildStruct.Something: string"}) } + +@(test) +ast_completion_fake_method_simple :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "methods" + main :: proc() { + n: int + n.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + double :: proc(x: int) -> int { return x * 2 } + `, + }, + }, + config = {enable_fake_method = true}, + } + // Should show 'double' as a fake method for int + test.expect_completion_labels(t, &source, ".", {"double"}) +} + +@(test) +ast_completion_fake_method_proc_group :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "methods" + main :: proc() { + n: int + n.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + add_int :: proc(a, b: int) -> int { return a + b } + add_something :: proc(a: int, b: string) {} + add_float :: proc(a, b: f32) -> f32 { return a + b } + add :: proc { add_float, add_int, add_something } + `, + }, + }, + config = {enable_fake_method = true}, + } + // Should show 'add' (the proc group), not 'add_int' or 'add_something' (individual procs) + test.expect_completion_labels(t, &source, ".", {"add"}, {"add_int", "add_something"}) +} + +@(test) +ast_completion_fake_method_proc_group_only_shows_group :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "methods" + main :: proc() { + s: methods.My_Struct + s.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + My_Struct :: struct { x: int } + + do_thing_int :: proc(s: My_Struct, v: int) {} + do_thing_str :: proc(s: My_Struct, v: string) {} + do_thing :: proc { do_thing_int, do_thing_str } + + // standalone proc not in a group + standalone_method :: proc(s: My_Struct) {} + `, + }, + }, + config = {enable_fake_method = true}, + } + // Should show 'do_thing' (group) and 'standalone_method', but NOT 'do_thing_int' or 'do_thing_str' + test.expect_completion_labels(t, &source, ".", {"do_thing", "standalone_method"}, {"do_thing_int", "do_thing_str"}) +} + +@(test) +ast_completion_fake_method_proc_group_with_only_one_proc :: proc(t: ^testing.T) { + // This is to verify that even if a proc group has only one member, + // it still shows up as a group and does not show the individual proc. + source := test.Source { + main = `package test + import "methods" + main :: proc() { + s: methods.My_Struct + s.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + My_Struct :: struct { x: int } + + do_thing_int :: proc(s: My_Struct, v: int) {} + do_thing :: proc { do_thing_int } + + // standalone proc not in a group + standalone_method :: proc(s: My_Struct) {} + `, + }, + }, + config = {enable_fake_method = true}, + } + + test.expect_completion_labels(t, &source, ".", {"do_thing", "standalone_method"}, {"do_thing_int" }) +} + +@(test) +ast_completion_fake_method_builtin_type_uses_builtin_pkg :: proc(t: ^testing.T) { + // This test verifies that fake methods for builtin types (int, f32, string, etc.) + // are correctly looked up using "$builtin" as the package, not the package where + // the variable is declared. Without this fix, the method lookup would fail because: + // - Storage: method stored with key {pkg = "$builtin", name = "int"} + // - Lookup (wrong): would use {pkg = "test", name = "int"} based on variable's declaring package + // - Lookup (correct): uses {pkg = "$builtin", name = "int"} for builtin types + source := test.Source { + main = `package test + import "math_utils" + main :: proc() { + x: f32 + x.{*} + } + `, + packages = { + { + pkg = "math_utils", + source = `package math_utils + square :: proc(v: f32) -> f32 { return v * v } + cube :: proc(v: f32) -> f32 { return v * v * v } + `, + }, + }, + config = {enable_fake_method = true}, + } + // Both methods should appear as fake methods for f32, proving that + // the lookup correctly uses "$builtin" instead of "test" for the package + test.expect_completion_labels(t, &source, ".", {"square", "cube"}) +} + +@(test) +ast_completion_fake_method_proc_group_single_arg_cursor_position :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "methods" + main :: proc() { + n: int + n.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + // All members only take a single argument (the receiver) + negate_a :: proc(x: int) -> int { return -x } + negate_b :: proc(x: int) -> int { return 0 - x } + negate :: proc { negate_a, negate_b } + `, + }, + }, + config = {enable_fake_method = true}, + } + // The proc group 'negate' should have cursor AFTER parentheses since no additional args + test.expect_completion_edit_text(t, &source, ".", "negate", "methods.negate(n)$0") +} |