✨ 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:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user