aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/server/collector.odin285
-rw-r--r--src/server/methods.odin281
-rw-r--r--src/testing/testing.odin3
-rw-r--r--tests/completions_test.odin114
4 files changed, 558 insertions, 125 deletions
diff --git a/src/server/collector.odin b/src/server/collector.odin
index d8a4e65..dc7e052 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,187 @@ 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
+ // Skip procedures that are part of proc groups
+ 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
+ // Only add once per distinct method key
+ 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:
+ // Check if this is a builtin type
+ 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 {
+ // Check all builtin type names from untyped_map
+ 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 +696,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 +757,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 +872,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 +911,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 +925,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..797a089 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,203 @@ 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)
+
+ // Proc groups always have multiple args (the receiver plus at least one overload's additional args)
+ new_text := build_method_call_text(ast_context, symbol, receiver, references, dereferences, true)
+
+ 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..20327a7 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)
diff --git a/tests/completions_test.odin b/tests/completions_test.odin
index 19f5363..8ed9b43 100644
--- a/tests/completions_test.odin
+++ b/tests/completions_test.odin
@@ -5327,3 +5327,117 @@ 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_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"})
+}