feat(lsp_server): add call snippets to completion resolve

This commit is contained in:
csh 2025-12-24 15:13:10 +08:00
parent baaf2ee688
commit b2a1bb6685
3 changed files with 575 additions and 71 deletions

View File

@ -168,6 +168,40 @@ namespace lsp::provider::completion_item
return best; return best;
} }
template<typename Callable>
const Callable* PickBestCallable(const std::vector<const Callable*>& candidates)
{
const Callable* best = nullptr;
std::size_t best_required = std::numeric_limits<std::size_t>::max();
std::size_t best_total = std::numeric_limits<std::size_t>::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<language::symbol::Parameter>& params, const std::optional<std::string>& return_type) std::string BuildSignature(const std::vector<language::symbol::Parameter>& params, const std::optional<std::string>& return_type)
{ {
std::string detail = "("; std::string detail = "(";
@ -208,6 +242,27 @@ namespace lsp::provider::completion_item
return snippet; return snippet;
} }
std::string BuildCallSnippet(const std::string& name, const std::vector<language::symbol::Parameter>& 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, std::string BuildCreateObjectSnippet(const std::string& class_name,
const language::symbol::Method* ctor, const language::symbol::Method* ctor,
bool has_open_quote, bool has_open_quote,
@ -235,6 +290,85 @@ namespace lsp::provider::completion_item
snippet += "$0"; snippet += "$0";
return snippet; return snippet;
} }
std::vector<const language::symbol::Function*> CollectFunctions(
const language::symbol::SymbolTable& table,
const std::string& function_name)
{
std::vector<const language::symbol::Function*> 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<language::symbol::Function>())
{
result.push_back(fn);
}
}
return result;
}
std::vector<const language::symbol::Method*> CollectMethods(
const language::symbol::SymbolTable& table,
language::symbol::SymbolId class_id,
const std::string& method_name,
std::optional<bool> is_static_filter)
{
std::vector<const language::symbol::Method*> 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<language::symbol::Method>();
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<protocol::LSPObject>(); const auto& obj = item.data->Get<protocol::LSPObject>();
auto ctx = GetStringField(obj, "ctx"); auto ctx = GetStringField(obj, "ctx");
auto class_name = GetStringField(obj, "class");
auto unit_name = GetStringField(obj, "unit");
auto uri = GetStringField(obj, "uri"); auto uri = GetStringField(obj, "uri");
if (ctx && class_name && !class_name->empty() && uri) if (ctx && uri)
{ {
auto& hub = execution_context.GetManagerHub(); auto& hub = execution_context.GetManagerHub();
@ -270,6 +402,17 @@ namespace lsp::provider::completion_item
auto workspace_tables = hub.symbols().GetWorkspaceSymbolTables(); auto workspace_tables = hub.symbols().GetWorkspaceSymbolTables();
auto system_tables = hub.symbols().GetSystemSymbolTables(); auto system_tables = hub.symbols().GetSystemSymbolTables();
if (*ctx == "new" || *ctx == "createobject")
{
auto class_name = GetStringField(obj, "class");
auto unit_name = GetStringField(obj, "unit");
if (!class_name || class_name->empty())
{
// Nothing to resolve.
}
else
{
auto try_find = [&](const language::symbol::SymbolTable& table) -> std::optional<const language::symbol::SymbolTable*> { auto try_find = [&](const language::symbol::SymbolTable& table) -> std::optional<const language::symbol::SymbolTable*> {
if (unit_name && !unit_name->empty()) if (unit_name && !unit_name->empty())
{ {
@ -356,6 +499,183 @@ namespace lsp::provider::completion_item
} }
} }
} }
else if (*ctx == "call")
{
auto callable_name = GetStringField(obj, "name");
if (!callable_name || callable_name->empty())
{
callable_name = item.label;
}
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<const language::symbol::SymbolTable*> {
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<const language::symbol::Function*> {
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;
}
}
}
}
}
protocol::ResponseMessage response; protocol::ResponseMessage response;
response.id = request.id; response.id = request.id;

View File

@ -414,6 +414,7 @@ namespace lsp::provider::text_document
void AppendFunctions(const language::symbol::SymbolTable& table, const std::string& prefix, CompletionSource source, std::vector<SourcedCompletionItem>& out) void AppendFunctions(const language::symbol::SymbolTable& table, const std::string& prefix, CompletionSource source, std::vector<SourcedCompletionItem>& out)
{ {
const auto module_name = GetModuleName(table);
for (const auto& wrapper : table.all_definitions()) for (const auto& wrapper : table.all_definitions())
{ {
const auto& symbol = wrapper.get(); const auto& symbol = wrapper.get();
@ -431,6 +432,17 @@ namespace lsp::provider::text_document
auto detail = BuildSignature(func->parameters, func->return_type); auto detail = BuildSignature(func->parameters, func->return_type);
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = detail, .description = SourceTag(source) }; 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 }); out.push_back({ std::move(item), source });
} }
} }
@ -467,6 +479,13 @@ namespace lsp::provider::text_document
{ {
auto detail = BuildSignature(func->parameters, func->return_type); auto detail = BuildSignature(func->parameters, func->return_type);
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = detail, .description = SourceTag(source) }; 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 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<SourcedCompletionItem>& out) void AppendClassMethods(const language::symbol::SymbolTable& table, const language::symbol::Class& cls, const std::string& prefix, CompletionSource source, std::vector<SourcedCompletionItem>& out)
{ {
const auto module_name = GetModuleName(table);
auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, cls.id); auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, cls.id);
if (!scope_id) if (!scope_id)
{ {
@ -857,6 +877,19 @@ namespace lsp::provider::text_document
.detail = BuildSignature(method->parameters, method->return_type), .detail = BuildSignature(method->parameters, method->return_type),
.description = SourceTag(source) .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 }); out.push_back({ std::move(item), source });
} }
} }
@ -932,6 +965,13 @@ namespace lsp::provider::text_document
CompletionSource source, CompletionSource source,
std::vector<SourcedCompletionItem>& out) std::vector<SourcedCompletionItem>& 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); auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, class_id);
if (!scope_id) if (!scope_id)
{ {
@ -979,6 +1019,22 @@ namespace lsp::provider::text_document
.detail = BuildSignature(method->parameters, method->return_type), .detail = BuildSignature(method->parameters, method->return_type),
.description = SourceTag(source) .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 }); out.push_back({ std::move(item), source });
continue; continue;
} }

View File

@ -51,6 +51,10 @@ export namespace lsp::test::provider
static TestResult TestCompletionResolveCreateObjectSnippet(); static TestResult TestCompletionResolveCreateObjectSnippet();
static TestResult TestCompletionResolveCreateObjectUnquotedSnippet(); static TestResult TestCompletionResolveCreateObjectUnquotedSnippet();
static TestResult TestCompletionResolveCreateObjectSingleQuoteSnippet(); 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 snippet", TestCompletionResolveCreateObjectSnippet);
runner.addTest("completion resolve createobject unquoted snippet", TestCompletionResolveCreateObjectUnquotedSnippet); runner.addTest("completion resolve createobject unquoted snippet", TestCompletionResolveCreateObjectUnquotedSnippet);
runner.addTest("completion resolve createobject single quote snippet", TestCompletionResolveCreateObjectSingleQuoteSnippet); 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() TestResult CompletionTests::TestClassMethodCompletion()
@ -681,4 +689,124 @@ namespace lsp::test::provider
assertTrue(snippet.contains("${1:a}"), "Resolved createobject snippet should include param placeholders"); assertTrue(snippet.contains("${1:a}"), "Resolved createobject snippet should include param placeholders");
return result; 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()<protocol::CompletionItem>(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()<protocol::CompletionItem>(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()<protocol::CompletionItem>(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()<protocol::CompletionItem>(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;
}
} }