diff --git a/lsp-server/src/language/semantic/analyzer.cppm b/lsp-server/src/language/semantic/analyzer.cppm index 4b8f62c..de2f989 100644 --- a/lsp-server/src/language/semantic/analyzer.cppm +++ b/lsp-server/src/language/semantic/analyzer.cppm @@ -1158,6 +1158,25 @@ namespace lsp::language::semantic { if (auto* ident = dynamic_cast(call->callee.get())) { + if (utils::IEquals(ident->name, "createobject")) + { + if (!call->arguments.empty() && call->arguments.front().value) + { + if (auto* lit = dynamic_cast(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) { diff --git a/lsp-server/src/language/symbol/internal/builder.cppm b/lsp-server/src/language/symbol/internal/builder.cppm index 2ef8962..e94c906 100644 --- a/lsp-server/src/language/symbol/internal/builder.cppm +++ b/lsp-server/src/language/symbol/internal/builder.cppm @@ -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 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 ExtractClassNameFromExpression(const ast::Expression* expr) + { + if (!expr) + { + return std::nullopt; + } + + if (const auto* ident = dynamic_cast(expr)) + { + if (!ident->name.empty()) + { + return ident->name; + } + return std::nullopt; + } + + if (const auto* attr = dynamic_cast(expr)) + { + // Handle `unit(x).ClassName` by taking last attribute segment. + return ExtractClassNameFromExpression(attr->attribute.get()); + } + + if (const auto* call = dynamic_cast(expr)) + { + return ExtractClassNameFromExpression(call->callee.get()); + } + + return std::nullopt; + } + + std::optional InferTypeFromExpression(const ast::Expression* expr) + { + if (!expr) + { + return std::nullopt; + } + + if (const auto* new_expr = dynamic_cast(expr)) + { + return ExtractClassNameFromExpression(new_expr->target.get()); + } + + if (const auto* call = dynamic_cast(expr)) + { + const auto* callee_ident = dynamic_cast(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(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(table.definition(*id_opt)); + if (!symbol) + { + return; + } + + if (auto* var = symbol->As()) + { + var->type = type_name; + } + else if (auto* field = symbol->As()) + { + 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 inferred_type; + if (node.right) + { + inferred_type = InferTypeFromExpression(node.right.get()); + } + + if (const auto* ident = std::get_if>(&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>(&node.left)) + { + if (*ident) + { + MaybeUpdateSymbolType(table_, current_scope_id_, (*ident)->name, *inferred_type); + } + } + } } } diff --git a/lsp-server/src/manager/symbol.cppm b/lsp-server/src/manager/symbol.cppm index 0384bde..11758d3 100644 --- a/lsp-server/src/manager/symbol.cppm +++ b/lsp-server/src/manager/symbol.cppm @@ -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(*analysis.symbol_table); + { + language::semantic::Analyzer analyzer(*analysis.symbol_table, *analysis.semantic_model); + analyzer.Analyze(*analysis.ast); + } + { std::unique_lock lock(mutex_); editing_symbols_[event.item.uri] = std::move(analysis); diff --git a/lsp-server/src/provider/completion_item/resolve.cppm b/lsp-server/src/provider/completion_item/resolve.cppm index 133001c..674c33c 100644 --- a/lsp-server/src/provider/completion_item/resolve.cppm +++ b/lsp-server/src/provider/completion_item/resolve.cppm @@ -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 GetStringField(const protocol::LSPObject& obj, const std::string& key) + { + auto it = obj.find(key); + if (it == obj.end() || !it->second.Is()) + { + return std::nullopt; + } + const auto& s = it->second.Get(); + return s; + } + + std::optional GetBoolField(const protocol::LSPObject& obj, const std::string& key) + { + auto it = obj.find(key); + if (it == obj.end() || !it->second.Is()) + { + return std::nullopt; + } + return it->second.Get(); + } + + 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 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 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 CollectConstructors( + const language::symbol::SymbolTable& table, + language::symbol::SymbolId class_id) + { + 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* member = table.definition(id); + if (!member || member->kind() != protocol::SymbolKind::Method) + { + continue; + } + + const auto* method = member->As(); + if (!method || method->method_kind != language::ast::MethodKind::kConstructor) + { + continue; + } + + result.push_back(method); + } + } + return result; + } + + const language::symbol::Method* PickBestConstructor(const std::vector& ctors) + { + const language::symbol::Method* best = nullptr; + std::size_t best_required = std::numeric_limits::max(); + std::size_t best_total = std::numeric_limits::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& params, const std::optional& 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(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()(request.params.value()); - // 暂未迁移 resolve 逻辑,保持原样返回 + if (item.data && item.data->Is()) + { + 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) + { + 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 { + 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); diff --git a/lsp-server/src/provider/text_document/completion.cppm b/lsp-server/src/provider/text_document/completion.cppm index bea972f..99737f4 100644 --- a/lsp-server/src/provider/text_document/completion.cppm +++ b/lsp-server/src/provider/text_document/completion.cppm @@ -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(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 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(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(); - 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(); + if (method && method->is_static) + return true; + } + } } return false; } + std::optional 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(); - 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 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(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& 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(); - 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(); + 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 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()) + { + if (const auto* class_symbol = table.definition(class_type->class_id())) + { + return class_symbol->name(); + } + } + } + } + + if (const auto* var = sym->As()) + { + return var->type; + } + + if (const auto* field = sym->As()) + { + 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& 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(); + 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(); + 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& 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 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()) + { + continue; + } + + auto& obj = entry.item.data->Get(); + auto ctx_it = obj.find("ctx"); + if (ctx_it == obj.end()) + { + continue; + } + + if (!ctx_it->second.Is()) + { + continue; + } + + obj["uri"] = params.textDocument.uri; + } + auto filtered = FilterAndSort(collected, context.prefix); list.items.reserve(filtered.size()); for (auto& item : filtered)