diff --git a/lsp-server/src/provider/completion_item/resolve.cppm b/lsp-server/src/provider/completion_item/resolve.cppm index 9f74a21..5036313 100644 --- a/lsp-server/src/provider/completion_item/resolve.cppm +++ b/lsp-server/src/provider/completion_item/resolve.cppm @@ -168,6 +168,40 @@ namespace lsp::provider::completion_item return best; } + template + const Callable* PickBestCallable(const std::vector& candidates) + { + const Callable* best = nullptr; + std::size_t best_required = std::numeric_limits::max(); + std::size_t best_total = std::numeric_limits::max(); + + for (const auto* candidate : candidates) + { + if (!candidate) + { + continue; + } + + std::size_t required = 0; + for (const auto& p : candidate->parameters) + { + if (!p.default_value.has_value()) + { + ++required; + } + } + + if (required < best_required || (required == best_required && candidate->parameters.size() < best_total)) + { + best = candidate; + best_required = required; + best_total = candidate->parameters.size(); + } + } + + return best; + } + std::string BuildSignature(const std::vector& params, const std::optional& return_type) { std::string detail = "("; @@ -208,6 +242,27 @@ namespace lsp::provider::completion_item return snippet; } + std::string BuildCallSnippet(const std::string& name, const std::vector& params) + { + std::string snippet = name; + snippet += "("; + + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i > 0) + { + snippet += ", "; + } + + const auto& p = params[i]; + snippet += "${" + std::to_string(i + 1) + ":" + p.name + "}"; + } + + snippet += ")"; + snippet += "$0"; + return snippet; + } + std::string BuildCreateObjectSnippet(const std::string& class_name, const language::symbol::Method* ctor, bool has_open_quote, @@ -235,6 +290,85 @@ namespace lsp::provider::completion_item snippet += "$0"; return snippet; } + + std::vector CollectFunctions( + const language::symbol::SymbolTable& table, + const std::string& function_name) + { + std::vector result; + for (auto id : table.FindSymbolsByName(function_name)) + { + const auto* symbol = table.definition(id); + if (!symbol || symbol->kind() != protocol::SymbolKind::Function) + { + continue; + } + + if (const auto* fn = symbol->As()) + { + result.push_back(fn); + } + } + return result; + } + + std::vector CollectMethods( + const language::symbol::SymbolTable& table, + language::symbol::SymbolId class_id, + const std::string& method_name, + std::optional is_static_filter) + { + std::vector result; + auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, class_id); + if (!scope_id) + { + return result; + } + + const auto* scope = table.scopes().scope(*scope_id); + if (!scope) + { + return result; + } + + for (const auto& [_, ids] : scope->symbols) + { + for (auto id : ids) + { + const auto* symbol = table.definition(id); + if (!symbol || symbol->kind() != protocol::SymbolKind::Method) + { + continue; + } + + const auto* method = symbol->As(); + if (!method) + { + continue; + } + + if (method->method_kind == language::ast::MethodKind::kConstructor || + method->method_kind == language::ast::MethodKind::kDestructor) + { + continue; + } + + if (!utils::IEquals(method->name, method_name)) + { + continue; + } + + if (is_static_filter.has_value() && method->is_static != *is_static_filter) + { + continue; + } + + result.push_back(method); + } + } + + return result; + } } @@ -258,11 +392,9 @@ namespace lsp::provider::completion_item { const auto& obj = item.data->Get(); auto ctx = GetStringField(obj, "ctx"); - auto class_name = GetStringField(obj, "class"); - auto unit_name = GetStringField(obj, "unit"); auto uri = GetStringField(obj, "uri"); - if (ctx && class_name && !class_name->empty() && uri) + if (ctx && uri) { auto& hub = execution_context.GetManagerHub(); @@ -270,89 +402,277 @@ namespace lsp::provider::completion_item auto workspace_tables = hub.symbols().GetWorkspaceSymbolTables(); auto system_tables = hub.symbols().GetSystemSymbolTables(); - auto try_find = [&](const language::symbol::SymbolTable& table) -> std::optional { - if (unit_name && !unit_name->empty()) + if (*ctx == "new" || *ctx == "createobject") + { + auto class_name = GetStringField(obj, "class"); + auto unit_name = GetStringField(obj, "unit"); + + if (!class_name || class_name->empty()) { - auto module = GetModuleName(table); - if (module.empty() || !utils::IEquals(module, *unit_name)) - { + // Nothing to resolve. + } + else + { + auto try_find = [&](const language::symbol::SymbolTable& table) -> std::optional { + if (unit_name && !unit_name->empty()) + { + auto module = GetModuleName(table); + if (module.empty() || !utils::IEquals(module, *unit_name)) + { + return std::nullopt; + } + } + if (FindClassSymbol(table, *class_name)) + { + return &table; + } return std::nullopt; - } - } - if (FindClassSymbol(table, *class_name)) - { - return &table; - } - return std::nullopt; - }; + }; - const language::symbol::SymbolTable* table_for_class = nullptr; - if (editing_table) - { - if (auto t = try_find(*editing_table)) - table_for_class = *t; - } - if (!table_for_class) - { - for (const auto* t : workspace_tables) - { - if (!t) - continue; - if (auto found = try_find(*t)) + const language::symbol::SymbolTable* table_for_class = nullptr; + if (editing_table) { - table_for_class = *found; - break; + if (auto t = try_find(*editing_table)) + table_for_class = *t; } - } - } - if (!table_for_class) - { - for (const auto* t : system_tables) - { - if (!t) - continue; - if (auto found = try_find(*t)) + if (!table_for_class) { - table_for_class = *found; - break; + for (const auto* t : workspace_tables) + { + if (!t) + continue; + if (auto found = try_find(*t)) + { + table_for_class = *found; + break; + } + } } - } - } - - const language::symbol::Method* best_ctor = nullptr; - if (table_for_class) - { - if (auto cls_sym = FindClassSymbol(*table_for_class, *class_name)) - { - auto ctors = CollectConstructors(*table_for_class, (*cls_sym)->id()); - best_ctor = PickBestConstructor(ctors); - - if (!item.labelDetails) + if (!table_for_class) { - item.labelDetails = protocol::CompletionItemLabelDetails{}; + for (const auto* t : system_tables) + { + if (!t) + continue; + if (auto found = try_find(*t)) + { + table_for_class = *found; + break; + } + } + } + + const language::symbol::Method* best_ctor = nullptr; + if (table_for_class) + { + if (auto cls_sym = FindClassSymbol(*table_for_class, *class_name)) + { + auto ctors = CollectConstructors(*table_for_class, (*cls_sym)->id()); + best_ctor = PickBestConstructor(ctors); + + if (!item.labelDetails) + { + item.labelDetails = protocol::CompletionItemLabelDetails{}; + } + item.labelDetails->detail = best_ctor ? BuildSignature(best_ctor->parameters, best_ctor->return_type) : ""; + } + } + + if (*ctx == "new") + { + item.insertText = BuildNewSnippet(*class_name, best_ctor); + item.insertTextFormat = protocol::InsertTextFormat::Snippet; + item.kind = protocol::CompletionItemKind::Constructor; + } + else if (*ctx == "createobject") + { + bool has_open_quote = GetBoolField(obj, "has_open_quote").value_or(false); + char quote_char = '"'; + if (auto quote = GetStringField(obj, "quote"); quote && !quote->empty()) + { + quote_char = (*quote)[0]; + } + + item.insertText = BuildCreateObjectSnippet(*class_name, best_ctor, has_open_quote, quote_char); + item.insertTextFormat = protocol::InsertTextFormat::Snippet; + item.kind = protocol::CompletionItemKind::Constructor; } - item.labelDetails->detail = best_ctor ? BuildSignature(best_ctor->parameters, best_ctor->return_type) : ""; } } - - if (*ctx == "new") + else if (*ctx == "call") { - item.insertText = BuildNewSnippet(*class_name, best_ctor); - item.insertTextFormat = protocol::InsertTextFormat::Snippet; - item.kind = protocol::CompletionItemKind::Constructor; - } - else if (*ctx == "createobject") - { - bool has_open_quote = GetBoolField(obj, "has_open_quote").value_or(false); - char quote_char = '"'; - if (auto quote = GetStringField(obj, "quote"); quote && !quote->empty()) + auto callable_name = GetStringField(obj, "name"); + if (!callable_name || callable_name->empty()) { - quote_char = (*quote)[0]; + callable_name = item.label; } - item.insertText = BuildCreateObjectSnippet(*class_name, best_ctor, has_open_quote, quote_char); - item.insertTextFormat = protocol::InsertTextFormat::Snippet; - item.kind = protocol::CompletionItemKind::Constructor; + auto unit_name = GetStringField(obj, "unit"); + auto class_name = GetStringField(obj, "class"); + auto is_static = GetBoolField(obj, "is_static"); + + auto matches_unit = [&](const language::symbol::SymbolTable& table) -> bool { + if (!unit_name || unit_name->empty()) + { + return true; + } + auto module = GetModuleName(table); + return !module.empty() && utils::IEquals(module, *unit_name); + }; + + if (class_name && !class_name->empty()) + { + auto try_find = [&](const language::symbol::SymbolTable& table) -> std::optional { + if (!matches_unit(table)) + { + return std::nullopt; + } + if (FindClassSymbol(table, *class_name)) + { + return &table; + } + return std::nullopt; + }; + + const language::symbol::SymbolTable* table_for_class = nullptr; + if (editing_table) + { + if (auto found = try_find(*editing_table)) + { + table_for_class = *found; + } + } + + if (!table_for_class) + { + for (const auto* t : workspace_tables) + { + if (!t) + { + continue; + } + if (auto found = try_find(*t)) + { + table_for_class = *found; + break; + } + } + } + + if (!table_for_class) + { + for (const auto* t : system_tables) + { + if (!t) + { + continue; + } + if (auto found = try_find(*t)) + { + table_for_class = *found; + break; + } + } + } + + if (table_for_class) + { + if (auto cls_sym = FindClassSymbol(*table_for_class, *class_name)) + { + auto methods = CollectMethods(*table_for_class, (*cls_sym)->id(), *callable_name, is_static); + const auto* best = PickBestCallable(methods); + if (best) + { + if (!item.labelDetails) + { + item.labelDetails = protocol::CompletionItemLabelDetails{}; + } + if (!item.labelDetails->detail || item.labelDetails->detail->empty()) + { + item.labelDetails->detail = BuildSignature(best->parameters, best->return_type); + } + + item.insertText = BuildCallSnippet(best->name, best->parameters); + item.insertTextFormat = protocol::InsertTextFormat::Snippet; + } + } + } + } + else + { + auto try_find = [&](const language::symbol::SymbolTable& table) -> std::optional { + if (!matches_unit(table)) + { + return std::nullopt; + } + + auto candidates = CollectFunctions(table, *callable_name); + if (candidates.empty()) + { + return std::nullopt; + } + + if (auto best = PickBestCallable(candidates)) + { + return best; + } + return std::nullopt; + }; + + const language::symbol::Function* best = nullptr; + if (editing_table) + { + if (auto found = try_find(*editing_table)) + { + best = *found; + } + } + if (!best) + { + for (const auto* t : workspace_tables) + { + if (!t) + { + continue; + } + if (auto found = try_find(*t)) + { + best = *found; + break; + } + } + } + if (!best) + { + for (const auto* t : system_tables) + { + if (!t) + { + continue; + } + if (auto found = try_find(*t)) + { + best = *found; + break; + } + } + } + + if (best) + { + if (!item.labelDetails) + { + item.labelDetails = protocol::CompletionItemLabelDetails{}; + } + if (!item.labelDetails->detail || item.labelDetails->detail->empty()) + { + item.labelDetails->detail = BuildSignature(best->parameters, best->return_type); + } + + item.insertText = BuildCallSnippet(best->name, best->parameters); + item.insertTextFormat = protocol::InsertTextFormat::Snippet; + } + } } } } diff --git a/lsp-server/src/provider/text_document/completion.cppm b/lsp-server/src/provider/text_document/completion.cppm index a783c85..2317302 100644 --- a/lsp-server/src/provider/text_document/completion.cppm +++ b/lsp-server/src/provider/text_document/completion.cppm @@ -414,6 +414,7 @@ namespace lsp::provider::text_document void AppendFunctions(const language::symbol::SymbolTable& table, const std::string& prefix, CompletionSource source, std::vector& out) { + const auto module_name = GetModuleName(table); for (const auto& wrapper : table.all_definitions()) { const auto& symbol = wrapper.get(); @@ -431,6 +432,17 @@ namespace lsp::provider::text_document auto detail = BuildSignature(func->parameters, func->return_type); item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = detail, .description = SourceTag(source) }; } + + protocol::LSPObject data; + data["ctx"] = "call"; + data["kind"] = "function"; + data["name"] = symbol.name(); + if (!module_name.empty()) + { + data["unit"] = module_name; + } + item.data = std::move(data); + out.push_back({ std::move(item), source }); } } @@ -467,6 +479,13 @@ namespace lsp::provider::text_document { auto detail = BuildSignature(func->parameters, func->return_type); item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = detail, .description = SourceTag(source) }; + + protocol::LSPObject data; + data["ctx"] = "call"; + data["kind"] = "function"; + data["name"] = symbol.name(); + data["unit"] = module_name; + item.data = std::move(data); } else { @@ -819,6 +838,7 @@ namespace lsp::provider::text_document void AppendClassMethods(const language::symbol::SymbolTable& table, const language::symbol::Class& cls, const std::string& prefix, CompletionSource source, std::vector& out) { + const auto module_name = GetModuleName(table); auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, cls.id); if (!scope_id) { @@ -857,6 +877,19 @@ namespace lsp::provider::text_document .detail = BuildSignature(method->parameters, method->return_type), .description = SourceTag(source) }; + + protocol::LSPObject data; + data["ctx"] = "call"; + data["kind"] = "method"; + data["name"] = method->name; + data["class"] = cls.name; + if (!module_name.empty()) + { + data["unit"] = module_name; + } + data["is_static"] = method->is_static; + item.data = std::move(data); + out.push_back({ std::move(item), source }); } } @@ -932,6 +965,13 @@ namespace lsp::provider::text_document CompletionSource source, std::vector& out) { + const auto module_name = GetModuleName(table); + std::string class_name; + if (const auto* class_symbol = table.definition(class_id)) + { + class_name = class_symbol->name(); + } + auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, class_id); if (!scope_id) { @@ -979,6 +1019,22 @@ namespace lsp::provider::text_document .detail = BuildSignature(method->parameters, method->return_type), .description = SourceTag(source) }; + + protocol::LSPObject data; + data["ctx"] = "call"; + data["kind"] = "method"; + data["name"] = method->name; + if (!class_name.empty()) + { + data["class"] = class_name; + } + if (!module_name.empty()) + { + data["unit"] = module_name; + } + data["is_static"] = method->is_static; + item.data = std::move(data); + out.push_back({ std::move(item), source }); continue; } diff --git a/lsp-server/test/test_provider/completion_test.cppm b/lsp-server/test/test_provider/completion_test.cppm index aa83b41..3dec0bf 100644 --- a/lsp-server/test/test_provider/completion_test.cppm +++ b/lsp-server/test/test_provider/completion_test.cppm @@ -51,6 +51,10 @@ export namespace lsp::test::provider static TestResult TestCompletionResolveCreateObjectSnippet(); static TestResult TestCompletionResolveCreateObjectUnquotedSnippet(); static TestResult TestCompletionResolveCreateObjectSingleQuoteSnippet(); + static TestResult TestCompletionResolveFunctionSnippet(); + static TestResult TestCompletionResolveUnitMemberFunctionSnippet(); + static TestResult TestCompletionResolveInstanceMethodSnippet(); + static TestResult TestCompletionResolveStaticMethodSnippet(); }; } @@ -191,6 +195,10 @@ namespace lsp::test::provider runner.addTest("completion resolve createobject snippet", TestCompletionResolveCreateObjectSnippet); runner.addTest("completion resolve createobject unquoted snippet", TestCompletionResolveCreateObjectUnquotedSnippet); runner.addTest("completion resolve createobject single quote snippet", TestCompletionResolveCreateObjectSingleQuoteSnippet); + runner.addTest("completion resolve function snippet", TestCompletionResolveFunctionSnippet); + runner.addTest("completion resolve unit member function snippet", TestCompletionResolveUnitMemberFunctionSnippet); + runner.addTest("completion resolve instance method snippet", TestCompletionResolveInstanceMethodSnippet); + runner.addTest("completion resolve static method snippet", TestCompletionResolveStaticMethodSnippet); } TestResult CompletionTests::TestClassMethodCompletion() @@ -681,4 +689,124 @@ namespace lsp::test::provider assertTrue(snippet.contains("${1:a}"), "Resolved createobject snippet should include param placeholders"); return result; } + + TestResult CompletionTests::TestCompletionResolveFunctionSnippet() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + auto items = RequestCompletion(env, uri, content, "UnitF"); + auto item = FindItem(items, "UnitFunc"); + assertTrue(item.has_value(), "Expected UnitFunc completion"); + + protocol::RequestMessage request; + request.id = "r5"; + request.method = "completionItem/resolve"; + request.params = codec::ToLSPAny(*item); + + ::lsp::provider::completion_item::Resolve resolver; + auto json = resolver.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + auto resolved = codec::FromLSPAny.template operator()(response.result.value()); + + assertTrue(resolved.insertText.has_value(), "Resolved function item should have insertText"); + auto snippet = resolved.insertText.value(); + assertTrue(snippet.starts_with("UnitFunc("), "Resolved function snippet should start with UnitFunc("); + assertTrue(snippet.contains("${1:a}"), "Resolved function snippet should include param placeholders"); + return result; + } + + TestResult CompletionTests::TestCompletionResolveUnitMemberFunctionSnippet() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + auto items = RequestCompletion(env, uri, content, "MainUnit.Uni"); + auto item = FindItem(items, "UnitFunc"); + assertTrue(item.has_value(), "Expected UnitFunc completion from unit member context"); + + protocol::RequestMessage request; + request.id = "r6"; + request.method = "completionItem/resolve"; + request.params = codec::ToLSPAny(*item); + + ::lsp::provider::completion_item::Resolve resolver; + auto json = resolver.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + auto resolved = codec::FromLSPAny.template operator()(response.result.value()); + + assertTrue(resolved.insertText.has_value(), "Resolved unit member function should have insertText"); + auto snippet = resolved.insertText.value(); + assertTrue(snippet.starts_with("UnitFunc("), "Resolved unit member function snippet should start with UnitFunc("); + assertTrue(snippet.contains("${1:a}"), "Resolved unit member function snippet should include param placeholders"); + return result; + } + + TestResult CompletionTests::TestCompletionResolveInstanceMethodSnippet() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + auto items = RequestCompletion(env, uri, content, "obj.Fo"); + auto item = FindItem(items, "Foo"); + assertTrue(item.has_value(), "Expected Foo completion from object member context"); + + protocol::RequestMessage request; + request.id = "r7"; + request.method = "completionItem/resolve"; + request.params = codec::ToLSPAny(*item); + + ::lsp::provider::completion_item::Resolve resolver; + auto json = resolver.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + auto resolved = codec::FromLSPAny.template operator()(response.result.value()); + + assertTrue(resolved.insertText.has_value(), "Resolved method item should have insertText"); + auto snippet = resolved.insertText.value(); + assertTrue(snippet.starts_with("Foo("), "Resolved method snippet should start with Foo("); + assertTrue(snippet.contains("${1:x}"), "Resolved method snippet should include param placeholders"); + return result; + } + + TestResult CompletionTests::TestCompletionResolveStaticMethodSnippet() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + auto items = RequestCompletion(env, uri, content, "class(Widget).Sta"); + auto item = FindItem(items, "StaticFoo"); + assertTrue(item.has_value(), "Expected StaticFoo completion from class method context"); + + protocol::RequestMessage request; + request.id = "r8"; + request.method = "completionItem/resolve"; + request.params = codec::ToLSPAny(*item); + + ::lsp::provider::completion_item::Resolve resolver; + auto json = resolver.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + auto resolved = codec::FromLSPAny.template operator()(response.result.value()); + + assertTrue(resolved.insertText.has_value(), "Resolved static method should have insertText"); + auto snippet = resolved.insertText.value(); + assertTrue(snippet.starts_with("StaticFoo("), "Resolved static method snippet should start with StaticFoo("); + assertTrue(snippet.contains("${1:y}"), "Resolved static method snippet should include param placeholders"); + return result; + } }