feat(lsp_completion): complete object creation and members

- Add class-name completion for `new` and `createobject("..."`.
- Generate constructor parameter snippets in completionItem/resolve.
- Support `obj.` instance member completion (methods/properties/fields).
- Infer `new`/`createobject` result types in SemanticModel TypeSystem.
This commit is contained in:
csh 2025-12-14 09:54:18 +08:00
parent 476e83beb8
commit 3106ab9ce4
5 changed files with 1024 additions and 107 deletions

View File

@ -1158,6 +1158,25 @@ namespace lsp::language::semantic
{
if (auto* ident = dynamic_cast<ast::Identifier*>(call->callee.get()))
{
if (utils::IEquals(ident->name, "createobject"))
{
if (!call->arguments.empty() && call->arguments.front().value)
{
if (auto* lit = dynamic_cast<ast::Literal*>(call->arguments.front().value.get()))
{
if (lit->literal_kind == ast::LiteralKind::kString)
{
auto class_id = ResolveClassSymbol(lit->value, lit->location);
if (class_id)
{
return type_system.CreateClassType(*class_id);
}
}
}
}
return type_system.GetUnknownType();
}
auto callee_id = ResolveIdentifier(ident->name, ident->location);
if (callee_id)
{

View File

@ -7,9 +7,140 @@ module lsp.language.symbol:internal.builder;
import :types;
import lsp.language.ast;
import lsp.protocol.types;
import lsp.utils.string;
namespace lsp::language::symbol
{
namespace
{
std::optional<std::string> UnquoteStringLiteral(std::string value)
{
if (value.size() < 2)
{
return std::nullopt;
}
char first = value.front();
char last = value.back();
if ((first == '"' && last == '"') || (first == '\'' && last == '\''))
{
value.erase(value.begin());
value.pop_back();
return value;
}
return std::nullopt;
}
std::optional<std::string> ExtractClassNameFromExpression(const ast::Expression* expr)
{
if (!expr)
{
return std::nullopt;
}
if (const auto* ident = dynamic_cast<const ast::Identifier*>(expr))
{
if (!ident->name.empty())
{
return ident->name;
}
return std::nullopt;
}
if (const auto* attr = dynamic_cast<const ast::AttributeExpression*>(expr))
{
// Handle `unit(x).ClassName` by taking last attribute segment.
return ExtractClassNameFromExpression(attr->attribute.get());
}
if (const auto* call = dynamic_cast<const ast::CallExpression*>(expr))
{
return ExtractClassNameFromExpression(call->callee.get());
}
return std::nullopt;
}
std::optional<std::string> InferTypeFromExpression(const ast::Expression* expr)
{
if (!expr)
{
return std::nullopt;
}
if (const auto* new_expr = dynamic_cast<const ast::NewExpression*>(expr))
{
return ExtractClassNameFromExpression(new_expr->target.get());
}
if (const auto* call = dynamic_cast<const ast::CallExpression*>(expr))
{
const auto* callee_ident = dynamic_cast<const ast::Identifier*>(call->callee.get());
if (!callee_ident)
{
return std::nullopt;
}
if (!utils::IEquals(utils::ToLower(callee_ident->name), "createobject"))
{
return std::nullopt;
}
if (call->arguments.empty() || !call->arguments[0].value)
{
return std::nullopt;
}
const auto* literal = dynamic_cast<const ast::Literal*>(call->arguments[0].value.get());
if (!literal || literal->literal_kind != ast::LiteralKind::kString)
{
return std::nullopt;
}
if (auto unquoted = UnquoteStringLiteral(literal->value))
{
if (!unquoted->empty())
{
return *unquoted;
}
}
return std::nullopt;
}
return std::nullopt;
}
void MaybeUpdateSymbolType(SymbolTable& table, ScopeId scope_id, const std::string& name, const std::string& type_name)
{
if (name.empty() || type_name.empty())
{
return;
}
auto id_opt = table.scopes().FindSymbolInScopeChain(scope_id, name);
if (!id_opt)
{
return;
}
Symbol* symbol = const_cast<Symbol*>(table.definition(*id_opt));
if (!symbol)
{
return;
}
if (auto* var = symbol->As<Variable>())
{
var->type = type_name;
}
else if (auto* field = symbol->As<Field>())
{
field->type = type_name;
}
}
}
Builder::Builder(SymbolTable& table) : table_(table),
current_scope_id_(kInvalidScopeId),
in_interface_section_(false),
@ -544,7 +675,13 @@ namespace lsp::language::symbol
void Builder::VisitVarDeclaration(ast::VarDeclaration& node)
{
CreateSymbol(node.name, protocol::SymbolKind::Variable, node.location, ExtractTypeName(node.type));
auto type_hint = ExtractTypeName(node.type);
if (!type_hint && node.initializer && *node.initializer)
{
type_hint = InferTypeFromExpression(node.initializer->get());
}
CreateSymbol(node.name, protocol::SymbolKind::Variable, node.location, type_hint);
if (node.initializer && *node.initializer)
{
@ -702,11 +839,40 @@ namespace lsp::language::symbol
void Builder::VisitAssignmentExpression(ast::AssignmentExpression& node)
{
std::optional<std::string> inferred_type;
if (node.right)
{
inferred_type = InferTypeFromExpression(node.right.get());
}
if (const auto* ident = std::get_if<std::unique_ptr<ast::Identifier>>(&node.left))
{
if (*ident)
{
auto existing = table_.scopes().FindSymbolInScopeChain(current_scope_id_, (*ident)->name);
if (!existing)
{
CreateSymbol((*ident)->name, protocol::SymbolKind::Variable, (*ident)->location, inferred_type);
}
}
}
ProcessLValue(node.left, true);
if (node.right)
{
VisitExpression(*node.right);
if (inferred_type)
{
if (const auto* ident = std::get_if<std::unique_ptr<ast::Identifier>>(&node.left))
{
if (*ident)
{
MaybeUpdateSymbolType(table_, current_scope_id_, (*ident)->name, *inferred_type);
}
}
}
}
}

View File

@ -1,6 +1,5 @@
module;
export module lsp.manager.symbol;
import tree_sitter;
import spdlog;
@ -497,6 +496,11 @@ namespace lsp::manager
analysis.semantic_model = std::make_unique<language::semantic::SemanticModel>(*analysis.symbol_table);
{
language::semantic::Analyzer analyzer(*analysis.symbol_table, *analysis.semantic_model);
analyzer.Analyze(*analysis.ast);
}
{
std::unique_lock<std::shared_mutex> lock(mutex_);
editing_symbols_[event.item.uri] = std::move(analysis);

View File

@ -1,6 +1,5 @@
module;
export module lsp.provider.completion_item.resolve;
import spdlog;
@ -9,6 +8,9 @@ import std;
import lsp.protocol;
import lsp.codec.facade;
import lsp.provider.base.interface;
import lsp.language.symbol;
import lsp.language.ast;
import lsp.utils.string;
namespace transform = lsp::codec;
@ -27,6 +29,214 @@ export namespace lsp::provider::completion_item
namespace lsp::provider::completion_item
{
namespace
{
std::optional<std::string> GetStringField(const protocol::LSPObject& obj, const std::string& key)
{
auto it = obj.find(key);
if (it == obj.end() || !it->second.Is<protocol::string>())
{
return std::nullopt;
}
const auto& s = it->second.Get<protocol::string>();
return s;
}
std::optional<bool> GetBoolField(const protocol::LSPObject& obj, const std::string& key)
{
auto it = obj.find(key);
if (it == obj.end() || !it->second.Is<protocol::boolean>())
{
return std::nullopt;
}
return it->second.Get<protocol::boolean>();
}
std::string GetModuleName(const language::symbol::SymbolTable& table)
{
for (const auto& wrapper : table.all_definitions())
{
const auto& symbol = wrapper.get();
if (symbol.kind() == protocol::SymbolKind::Module)
{
return symbol.name();
}
}
return "";
}
std::optional<const language::symbol::Symbol*> FindClassSymbol(
const language::symbol::SymbolTable& table,
const std::string& class_name)
{
auto ids = table.FindSymbolsByName(class_name);
for (auto id : ids)
{
const auto* symbol = table.definition(id);
if (symbol && symbol->kind() == protocol::SymbolKind::Class)
{
return symbol;
}
}
return std::nullopt;
}
std::optional<language::symbol::ScopeId> FindScopeOwnedBy(
const language::symbol::SymbolTable& table,
language::symbol::ScopeKind kind,
language::symbol::SymbolId owner_id)
{
for (const auto& [scope_id, scope] : table.scopes().all_scopes())
{
if (scope.kind == kind && scope.owner && *scope.owner == owner_id)
{
return scope_id;
}
}
return std::nullopt;
}
std::vector<const language::symbol::Method*> CollectConstructors(
const language::symbol::SymbolTable& table,
language::symbol::SymbolId class_id)
{
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* member = table.definition(id);
if (!member || member->kind() != protocol::SymbolKind::Method)
{
continue;
}
const auto* method = member->As<language::symbol::Method>();
if (!method || method->method_kind != language::ast::MethodKind::kConstructor)
{
continue;
}
result.push_back(method);
}
}
return result;
}
const language::symbol::Method* PickBestConstructor(const std::vector<const language::symbol::Method*>& ctors)
{
const language::symbol::Method* 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* ctor : ctors)
{
if (!ctor)
{
continue;
}
std::size_t required = 0;
for (const auto& p : ctor->parameters)
{
if (!p.default_value.has_value())
{
++required;
}
}
if (required < best_required || (required == best_required && ctor->parameters.size() < best_total))
{
best = ctor;
best_required = required;
best_total = ctor->parameters.size();
}
}
return best;
}
std::string BuildSignature(const std::vector<language::symbol::Parameter>& params, const std::optional<std::string>& return_type)
{
std::string detail = "(";
for (std::size_t i = 0; i < params.size(); ++i)
{
if (i > 0)
detail += ", ";
detail += params[i].name;
if (params[i].type && !params[i].type->empty())
detail += ": " + *params[i].type;
}
detail += ")";
if (return_type && !return_type->empty())
detail += ": " + *return_type;
return detail;
}
std::string BuildNewSnippet(const std::string& class_name, const language::symbol::Method* ctor)
{
std::string snippet = class_name;
snippet += "(";
if (ctor && !ctor->parameters.empty())
{
for (std::size_t i = 0; i < ctor->parameters.size(); ++i)
{
if (i > 0)
{
snippet += ", ";
}
const auto& p = ctor->parameters[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,
char quote_char)
{
std::string snippet;
if (!has_open_quote)
{
snippet.push_back(quote_char);
}
snippet += class_name;
snippet.push_back(quote_char);
if (ctor && !ctor->parameters.empty())
{
for (std::size_t i = 0; i < ctor->parameters.size(); ++i)
{
snippet += ", ";
const auto& p = ctor->parameters[i];
snippet += "${" + std::to_string(i + 1) + ":" + p.name + "}";
}
}
snippet += "$0";
return snippet;
}
}
std::string Resolve::GetMethod() const
{
return "completionItem/resolve";
@ -40,8 +250,6 @@ namespace lsp::provider::completion_item
std::string Resolve::ProvideResponse(const protocol::RequestMessage& request,
ExecutionContext& execution_context)
{
static_cast<void>(execution_context);
if (!request.params.has_value())
{
spdlog::warn("{}: Missing params in request", GetProviderName());
@ -52,7 +260,109 @@ namespace lsp::provider::completion_item
protocol::CompletionItem item = transform::FromLSPAny.template operator()<protocol::CompletionItem>(request.params.value());
// 暂未迁移 resolve 逻辑,保持原样返回
if (item.data && item.data->Is<protocol::LSPObject>())
{
const auto& obj = item.data->Get<protocol::LSPObject>();
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)
{
auto& hub = execution_context.GetManagerHub();
const language::symbol::SymbolTable* editing_table = hub.symbols().GetSymbolTable(*uri);
auto workspace_tables = hub.symbols().GetWorkspaceSymbolTables();
auto system_tables = hub.symbols().GetSystemSymbolTables();
auto try_find = [&](const language::symbol::SymbolTable& table) -> std::optional<const language::symbol::SymbolTable*> {
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;
};
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))
{
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;
}
}
}
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;
}
}
}
protocol::ResponseMessage response;
response.id = request.id;
response.result = transform::ToLSPAny(item);

View File

@ -12,6 +12,7 @@ import lsp.codec.facade;
import lsp.provider.base.interface;
import lsp.language.ast;
import lsp.language.symbol;
import lsp.language.semantic;
import lsp.language.keyword;
import lsp.utils.string;
import lsp.utils.text_coordinates;
@ -49,13 +50,18 @@ namespace lsp::provider::text_document
std::string prefix;
std::string class_name;
bool is_new_context = false;
bool is_createobject_context = false;
bool is_unit_context = false;
bool is_class_context = false;
bool is_class_method_context = false;
bool is_unit_member_context = false;
bool is_object_member_context = false;
bool is_unit_scoped_new = false;
std::string unit_name;
std::string member_prefix;
std::string object_name;
bool createobject_has_open_quote = false;
char createobject_quote = '"';
};
struct SourcedCompletionItem
@ -85,14 +91,17 @@ namespace lsp::provider::text_document
switch (kind)
{
case protocol::SymbolKind::Function:
case protocol::SymbolKind::Method:
return protocol::CompletionItemKind::Function;
case protocol::SymbolKind::Method:
return protocol::CompletionItemKind::Method;
case protocol::SymbolKind::Constructor:
return protocol::CompletionItemKind::Constructor;
case protocol::SymbolKind::Class:
return protocol::CompletionItemKind::Class;
case protocol::SymbolKind::Property:
return protocol::CompletionItemKind::Property;
case protocol::SymbolKind::Field:
return protocol::CompletionItemKind::Field;
case protocol::SymbolKind::Variable:
case protocol::SymbolKind::TypeParameter:
return protocol::CompletionItemKind::Variable;
@ -133,6 +142,60 @@ namespace lsp::provider::text_document
return after_new.find('(') == std::string::npos;
}
struct CreateObjectContextInfo
{
std::string prefix;
bool has_open_quote = false;
char quote = '"';
};
bool IsCreateObjectContext(const std::string& line, CreateObjectContextInfo& out)
{
std::string line_lower = utils::ToLower(line);
std::size_t pos = line_lower.rfind("createobject(");
if (pos == std::string::npos)
{
return false;
}
std::string after = line.substr(pos + std::string("createobject(").size());
std::size_t i = 0;
while (i < after.size() && std::isspace(static_cast<unsigned char>(after[i])))
{
++i;
}
after = after.substr(i);
if (after.empty())
{
out = CreateObjectContextInfo{};
return true;
}
if (after.find(',') != std::string::npos)
{
return false;
}
char first = after.front();
if (first == '"' || first == '\'')
{
out.has_open_quote = true;
out.quote = first;
out.prefix = after.substr(1);
if (out.prefix.find(first) != std::string::npos)
{
return false;
}
return true;
}
out.prefix = after;
out.has_open_quote = false;
out.quote = '"';
return true;
}
bool IsClassMethodContext(const std::string& line, std::string& out_class_name, std::string& out_prefix)
{
std::string line_lower = utils::ToLower(line);
@ -179,57 +242,6 @@ namespace lsp::provider::text_document
return after.find(')') == std::string::npos;
}
std::optional<CompletionContext> DetectUnitMemberContext(const std::string& line)
{
CompletionContext ctx;
// unit(xxx).prefix
{
std::string line_lower = utils::ToLower(line);
std::size_t unit_pos = line_lower.rfind("unit(");
if (unit_pos != std::string::npos)
{
std::string after_unit = line.substr(unit_pos + 5);
std::size_t close_paren = after_unit.find(')');
std::size_t dot_pos = after_unit.find('.');
if (dot_pos != std::string::npos && dot_pos > 0)
{
std::size_t name_end = close_paren != std::string::npos && close_paren < dot_pos ? close_paren : dot_pos;
ctx.unit_name = utils::Trim(after_unit.substr(0, name_end));
ctx.member_prefix = utils::Trim(after_unit.substr(dot_pos + 1));
ctx.is_unit_member_context = !ctx.unit_name.empty();
if (ctx.is_unit_member_context)
return ctx;
}
}
}
// alias.prefix
std::size_t dot_pos = line.rfind('.');
if (dot_pos != std::string::npos)
{
// find token before dot
std::size_t start = dot_pos;
while (start > 0)
{
char ch = line[start - 1];
if (!std::isalnum(static_cast<unsigned char>(ch)) && ch != '_')
break;
--start;
}
std::string alias = utils::Trim(line.substr(start, dot_pos - start));
std::string prefix = utils::Trim(line.substr(dot_pos + 1));
if (!alias.empty())
{
ctx.is_unit_member_context = true;
ctx.unit_name = alias;
ctx.member_prefix = prefix;
return ctx;
}
}
return std::nullopt;
}
std::string ExtractPrefixByScan(std::size_t cursor_pos, std::size_t line_start, const std::string& content)
{
std::size_t prefix_start = cursor_pos;
@ -279,18 +291,29 @@ namespace lsp::provider::text_document
}
else
{
result.is_new_context = IsNewContext(result.line_content);
result.is_unit_context = IsUnitContext(result.line_content);
result.is_class_context = IsClassContext(result.line_content);
if (result.is_new_context)
result.prefix = utils::Trim(result.line_content.substr(result.line_content.rfind("new ") + 4));
else if (result.is_unit_context)
result.prefix = utils::Trim(result.line_content.substr(result.line_content.rfind("unit(") + 5));
else if (result.is_class_context)
result.prefix = utils::Trim(result.line_content.substr(result.line_content.rfind("class(") + 6));
CreateObjectContextInfo create_info;
result.is_createobject_context = IsCreateObjectContext(result.line_content, create_info);
if (result.is_createobject_context)
{
result.prefix = utils::Trim(create_info.prefix);
result.createobject_has_open_quote = create_info.has_open_quote;
result.createobject_quote = create_info.quote;
}
else
result.prefix = ExtractPrefixByScan(cursor_pos, line_start, content);
{
result.is_new_context = IsNewContext(result.line_content);
result.is_unit_context = IsUnitContext(result.line_content);
result.is_class_context = IsClassContext(result.line_content);
if (result.is_new_context)
result.prefix = utils::Trim(result.line_content.substr(result.line_content.rfind("new ") + 4));
else if (result.is_unit_context)
result.prefix = utils::Trim(result.line_content.substr(result.line_content.rfind("unit(") + 5));
else if (result.is_class_context)
result.prefix = utils::Trim(result.line_content.substr(result.line_content.rfind("class(") + 6));
else
result.prefix = ExtractPrefixByScan(cursor_pos, line_start, content);
}
}
return result;
}
@ -458,18 +481,45 @@ namespace lsp::provider::text_document
bool HasStaticMethod(const language::symbol::Class& cls, const language::symbol::SymbolTable& table)
{
for (auto member_id : cls.members)
const auto& scopes = table.scopes().all_scopes();
for (const auto& [_, scope] : scopes)
{
const auto* member = table.definition(member_id);
if (!member || member->kind() != protocol::SymbolKind::Method)
if (scope.kind != language::symbol::ScopeKind::kClass || !scope.owner || *scope.owner != cls.id)
{
continue;
const auto* method = member->As<language::symbol::Method>();
if (method && method->is_static)
return true;
}
for (const auto& [_, ids] : scope.symbols)
{
for (auto id : ids)
{
const auto* member = table.definition(id);
if (!member || member->kind() != protocol::SymbolKind::Method)
continue;
const auto* method = member->As<language::symbol::Method>();
if (method && method->is_static)
return true;
}
}
}
return false;
}
std::optional<language::symbol::ScopeId> FindScopeOwnedBy(
const language::symbol::SymbolTable& table,
language::symbol::ScopeKind kind,
language::symbol::SymbolId owner_id)
{
for (const auto& [scope_id, scope] : table.scopes().all_scopes())
{
if (scope.kind == kind && scope.owner && *scope.owner == owner_id)
{
return scope_id;
}
}
return std::nullopt;
}
void AppendClasses(const language::symbol::SymbolTable& table,
const std::string& prefix,
CompletionSource source,
@ -477,9 +527,9 @@ namespace lsp::provider::text_document
bool mark_new_context,
const std::string& unit_filter = "")
{
auto module_name = GetModuleName(table);
if (!unit_filter.empty())
{
auto module_name = GetModuleName(table);
if (module_name.empty() || !utils::IEquals(module_name, unit_filter))
return;
}
@ -493,14 +543,23 @@ namespace lsp::provider::text_document
continue;
const auto* cls = symbol.As<language::symbol::Class>();
if (!cls || !HasStaticMethod(*cls, table))
if (!cls)
continue;
if (!mark_new_context && !HasStaticMethod(*cls, table))
continue;
protocol::CompletionItem item;
item.label = symbol.name();
item.kind = ToCompletionItemKind(symbol.kind());
item.kind = mark_new_context ? protocol::CompletionItemKind::Constructor : ToCompletionItemKind(symbol.kind());
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = "", .description = SourceTag(source) };
item.data = protocol::LSPAny(mark_new_context ? "new_context" : "class_context");
protocol::LSPObject data;
data["ctx"] = mark_new_context ? "new" : "class";
data["class"] = symbol.name();
data["unit"] = module_name;
item.data = std::move(data);
out.push_back({ std::move(item), source });
}
}
@ -544,32 +603,301 @@ namespace lsp::provider::text_document
return std::nullopt;
}
struct DotAccessContext
{
bool is_unit_call = false;
std::string base_name;
std::string member_prefix;
};
std::optional<DotAccessContext> DetectDotAccessContext(const std::string& line)
{
// unit(xxx).prefix
{
std::string line_lower = utils::ToLower(line);
std::size_t unit_pos = line_lower.rfind("unit(");
if (unit_pos != std::string::npos)
{
std::string after_unit = line.substr(unit_pos + 5);
std::size_t close_paren = after_unit.find(')');
if (close_paren == std::string::npos)
{
return std::nullopt;
}
std::size_t dot_pos = after_unit.find('.', close_paren);
if (dot_pos != std::string::npos && dot_pos > close_paren)
{
DotAccessContext ctx;
ctx.is_unit_call = true;
ctx.base_name = utils::Trim(after_unit.substr(0, close_paren));
ctx.member_prefix = utils::Trim(after_unit.substr(dot_pos + 1));
if (!ctx.base_name.empty())
{
return ctx;
}
}
}
}
// ident.prefix
std::size_t dot_pos = line.rfind('.');
if (dot_pos == std::string::npos)
{
return std::nullopt;
}
std::size_t start = dot_pos;
while (start > 0)
{
char ch = line[start - 1];
if (!std::isalnum(static_cast<unsigned char>(ch)) && ch != '_')
break;
--start;
}
std::string base = utils::Trim(line.substr(start, dot_pos - start));
if (base.empty())
{
return std::nullopt;
}
DotAccessContext ctx;
ctx.base_name = std::move(base);
ctx.member_prefix = utils::Trim(line.substr(dot_pos + 1));
return ctx;
}
void AppendClassMethods(const language::symbol::SymbolTable& table, const language::symbol::Class& cls, const std::string& prefix, CompletionSource source, std::vector<SourcedCompletionItem>& out)
{
for (auto member_id : cls.members)
auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, cls.id);
if (!scope_id)
{
const auto* member_symbol = table.definition(member_id);
if (!member_symbol || member_symbol->kind() != protocol::SymbolKind::Method)
continue;
return;
}
const auto* method = member_symbol->As<language::symbol::Method>();
if (!method)
continue;
const auto* scope = table.scopes().scope(*scope_id);
if (!scope)
{
return;
}
// 仅补全类方法(静态)
if (!method->is_static)
continue;
for (const auto& [_, ids] : scope->symbols)
{
for (auto id : ids)
{
const auto* member_symbol = table.definition(id);
if (!member_symbol || member_symbol->kind() != protocol::SymbolKind::Method)
continue;
if (!prefix.empty() && !utils::IStartsWith(method->name, prefix))
const auto* method = member_symbol->As<language::symbol::Method>();
if (!method)
continue;
// 仅补全类方法(静态)
if (!method->is_static)
continue;
if (!prefix.empty() && !utils::IStartsWith(method->name, prefix))
continue;
protocol::CompletionItem item;
item.label = method->name;
item.kind = protocol::CompletionItemKind::Method;
item.labelDetails = protocol::CompletionItemLabelDetails{
.detail = BuildSignature(method->parameters, method->return_type),
.description = SourceTag(source)
};
out.push_back({ std::move(item), source });
}
}
}
std::optional<std::string> ResolveTypeNameInScope(
const language::symbol::SymbolTable& table,
const language::semantic::SemanticModel* semantic,
const protocol::Position& position,
const std::string& content,
const std::string& name)
{
if (name.empty())
{
return std::nullopt;
}
std::uint32_t offset = utils::text_coordinates::ToOffset(position, content);
language::ast::Location loc{};
loc.start_line = loc.end_line = position.line;
loc.start_column = loc.end_column = position.character;
loc.start_offset = loc.end_offset = offset;
auto scope_id = table.scopes().FindScopeAt(loc);
if (!scope_id)
{
return std::nullopt;
}
auto symbol_id = table.scopes().FindSymbolInScopeChain(*scope_id, name);
if (!symbol_id)
{
return std::nullopt;
}
const auto* sym = table.definition(*symbol_id);
if (!sym)
{
return std::nullopt;
}
if (semantic)
{
auto type = semantic->type_system().GetSymbolType(*symbol_id);
if (type && type->kind() == language::semantic::TypeKind::kClass)
{
if (const auto* class_type = type->As<language::semantic::ClassType>())
{
if (const auto* class_symbol = table.definition(class_type->class_id()))
{
return class_symbol->name();
}
}
}
}
if (const auto* var = sym->As<language::symbol::Variable>())
{
return var->type;
}
if (const auto* field = sym->As<language::symbol::Field>())
{
return field->type;
}
return std::nullopt;
}
void AppendInstanceMembers(const language::symbol::SymbolTable& table,
language::symbol::SymbolId class_id,
const std::string& prefix,
CompletionSource source,
std::vector<SourcedCompletionItem>& out)
{
auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, class_id);
if (!scope_id)
{
return;
}
const auto* scope = table.scopes().scope(*scope_id);
if (!scope)
{
return;
}
for (const auto& [_, ids] : scope->symbols)
{
for (auto id : ids)
{
const auto* member = table.definition(id);
if (!member)
{
continue;
}
if (!prefix.empty() && !utils::IStartsWith(member->name(), prefix))
{
continue;
}
if (member->kind() == protocol::SymbolKind::Method)
{
const auto* method = member->As<language::symbol::Method>();
if (!method || method->is_static)
{
continue;
}
if (method->method_kind == language::ast::MethodKind::kConstructor ||
method->method_kind == language::ast::MethodKind::kDestructor)
{
continue;
}
protocol::CompletionItem item;
item.label = method->name;
item.kind = protocol::CompletionItemKind::Method;
item.labelDetails = protocol::CompletionItemLabelDetails{
.detail = BuildSignature(method->parameters, method->return_type),
.description = SourceTag(source)
};
out.push_back({ std::move(item), source });
continue;
}
if (member->kind() == protocol::SymbolKind::Property)
{
protocol::CompletionItem item;
item.label = member->name();
item.kind = protocol::CompletionItemKind::Property;
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = "", .description = SourceTag(source) };
out.push_back({ std::move(item), source });
continue;
}
if (member->kind() == protocol::SymbolKind::Field)
{
const auto* field = member->As<language::symbol::Field>();
if (!field || field->is_static)
{
continue;
}
protocol::CompletionItem item;
item.label = field->name;
item.kind = protocol::CompletionItemKind::Field;
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = "", .description = SourceTag(source) };
out.push_back({ std::move(item), source });
continue;
}
}
}
}
void AppendCreateObjectClasses(const language::symbol::SymbolTable& table,
const std::string& prefix,
CompletionSource source,
std::vector<SourcedCompletionItem>& out,
bool has_open_quote,
char quote_char,
const std::string& unit_filter = "")
{
auto module_name = GetModuleName(table);
if (!unit_filter.empty())
{
if (module_name.empty() || !utils::IEquals(module_name, unit_filter))
return;
}
for (const auto& wrapper : table.all_definitions())
{
const auto& symbol = wrapper.get();
if (symbol.kind() != protocol::SymbolKind::Class)
continue;
if (!prefix.empty() && !utils::IStartsWith(symbol.name(), prefix))
continue;
protocol::CompletionItem item;
item.label = method->name;
item.kind = protocol::CompletionItemKind::Function;
item.labelDetails = protocol::CompletionItemLabelDetails{
.detail = BuildSignature(method->parameters, method->return_type),
.description = SourceTag(source)
};
item.label = symbol.name();
item.kind = protocol::CompletionItemKind::Constructor;
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = "", .description = SourceTag(source) };
protocol::LSPObject data;
data["ctx"] = "createobject";
data["class"] = symbol.name();
data["unit"] = module_name;
data["has_open_quote"] = has_open_quote;
data["quote"] = std::string(1, quote_char);
item.data = std::move(data);
out.push_back({ std::move(item), source });
}
}
@ -650,21 +978,11 @@ namespace lsp::provider::text_document
const auto content = manager_hub.documents().GetContent(params.textDocument.uri);
auto context = AnalyzeContext(params, content);
if (!context.is_class_method_context)
{
if (auto unit_ctx = DetectUnitMemberContext(context.line_content))
{
context.is_unit_member_context = unit_ctx->is_unit_member_context;
context.unit_name = unit_ctx->unit_name;
context.member_prefix = unit_ctx->member_prefix;
context.prefix = unit_ctx->member_prefix;
}
}
std::vector<SourcedCompletionItem> collected;
// 当前文档符号表
const auto* editing_table = manager_hub.symbols().GetSymbolTable(params.textDocument.uri);
const auto* editing_semantic = manager_hub.symbols().GetSemanticModel(params.textDocument.uri);
// 工作区/系统符号表
auto workspace_tables = manager_hub.symbols().GetWorkspaceSymbolTables();
auto system_tables = manager_hub.symbols().GetSystemSymbolTables();
@ -677,6 +995,42 @@ namespace lsp::provider::text_document
visible_units_set.insert(u);
}
// Member access context (unit/obj)
if (!context.is_class_method_context &&
!context.is_new_context &&
!context.is_unit_context &&
!context.is_class_context &&
!context.is_createobject_context)
{
if (auto dot_ctx = DetectDotAccessContext(context.line_content))
{
context.prefix = dot_ctx->member_prefix;
context.member_prefix = dot_ctx->member_prefix;
if (dot_ctx->is_unit_call)
{
if (IsUnitVisible(visible_units_set, dot_ctx->base_name))
{
context.is_unit_member_context = true;
context.unit_name = dot_ctx->base_name;
}
}
else
{
if (IsUnitVisible(visible_units_set, dot_ctx->base_name))
{
context.is_unit_member_context = true;
context.unit_name = dot_ctx->base_name;
}
else
{
context.is_object_member_context = true;
context.object_name = dot_ctx->base_name;
}
}
}
}
// 处理 new unit(...) 或 alias.class
if (context.is_new_context && !context.prefix.empty())
{
@ -776,6 +1130,34 @@ namespace lsp::provider::text_document
AppendUnitMembers(*table, context.unit_name, context.member_prefix, CompletionSource::kSystem, collected);
}
}
else if (context.is_object_member_context)
{
if (editing_table && content)
{
if (auto type_name = ResolveTypeNameInScope(*editing_table, editing_semantic, params.position, *content, context.object_name))
{
bool found_class = false;
auto try_append = [&](const language::symbol::SymbolTable& table, CompletionSource source) {
auto cls_opt = FindClassSymbol(table, *type_name);
if (!cls_opt)
{
return;
}
AppendInstanceMembers(table, (*cls_opt)->id(), context.member_prefix, source, collected);
found_class = true;
};
try_append(*editing_table, CompletionSource::kEditing);
for (const auto* table : workspace_tables)
try_append(*table, CompletionSource::kWorkspace);
for (const auto* table : system_tables)
try_append(*table, CompletionSource::kSystem);
(void)found_class;
}
}
}
else if (context.is_new_context)
{
if (context.is_unit_scoped_new && !context.unit_name.empty())
@ -797,6 +1179,15 @@ namespace lsp::provider::text_document
AppendClasses(*table, context.prefix, CompletionSource::kSystem, collected, true);
}
}
else if (context.is_createobject_context)
{
if (editing_table)
AppendCreateObjectClasses(*editing_table, context.prefix, CompletionSource::kEditing, collected, context.createobject_has_open_quote, context.createobject_quote);
for (const auto* table : workspace_tables)
AppendCreateObjectClasses(*table, context.prefix, CompletionSource::kWorkspace, collected, context.createobject_has_open_quote, context.createobject_quote);
for (const auto* table : system_tables)
AppendCreateObjectClasses(*table, context.prefix, CompletionSource::kSystem, collected, context.createobject_has_open_quote, context.createobject_quote);
}
else if (context.is_unit_context)
{
if (!visible_units_set.empty())
@ -830,6 +1221,33 @@ namespace lsp::provider::text_document
AppendFunctions(*table, context.prefix, CompletionSource::kSystem, collected);
}
for (auto& entry : collected)
{
if (!entry.item.data)
{
continue;
}
if (!entry.item.data->Is<protocol::LSPObject>())
{
continue;
}
auto& obj = entry.item.data->Get<protocol::LSPObject>();
auto ctx_it = obj.find("ctx");
if (ctx_it == obj.end())
{
continue;
}
if (!ctx_it->second.Is<protocol::string>())
{
continue;
}
obj["uri"] = params.textDocument.uri;
}
auto filtered = FilterAndSort(collected, context.prefix);
list.items.reserve(filtered.size());
for (auto& item : filtered)