tsl-devkit/lsp-server/src/provider/text_document/completion.cppm

1540 lines
63 KiB
C++

module;
#include <cstdint>
export module lsp.provider.text_document.completion;
import spdlog;
import std;
import lsp.protocol;
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;
namespace transform = lsp::codec;
export namespace lsp::provider::text_document
{
class Completion : public AutoRegisterProvider<Completion, IRequestProvider>
{
public:
static constexpr std::string_view kMethod = "textDocument/completion";
static constexpr std::string_view kProviderName = "TextDocumentCompletion";
std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& execution_context) override;
};
}
namespace lsp::provider::text_document
{
namespace
{
enum class CompletionSource
{
kEditing,
kWorkspace,
kSystem,
kKeyword
};
struct CompletionContext
{
std::string uri;
protocol::Position position{};
std::string line_content;
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
{
protocol::CompletionItem item;
CompletionSource source;
};
std::string SourceTag(CompletionSource source)
{
switch (source)
{
case CompletionSource::kEditing:
return "[E]";
case CompletionSource::kWorkspace:
return "[W]";
case CompletionSource::kSystem:
return "[S]";
case CompletionSource::kKeyword:
default:
return "[K]";
}
}
protocol::CompletionItemKind ToCompletionItemKind(protocol::SymbolKind kind)
{
switch (kind)
{
case protocol::SymbolKind::Function:
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;
case protocol::SymbolKind::Constant:
return protocol::CompletionItemKind::Constant;
case protocol::SymbolKind::Module:
return protocol::CompletionItemKind::Module;
default:
return protocol::CompletionItemKind::Text;
}
}
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;
}
bool IsNewContext(const std::string& line)
{
std::string line_lower = utils::ToLower(line);
std::size_t new_pos = line_lower.rfind("new ");
if (new_pos == std::string::npos)
return false;
std::string after_new = utils::Trim(line.substr(new_pos + 4));
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);
std::size_t class_pos = line_lower.rfind("class(");
if (class_pos == std::string::npos)
return false;
std::string after_class = line.substr(class_pos + 6);
std::size_t close_paren = after_class.find(')');
if (close_paren == std::string::npos)
return false;
std::size_t dot_pos = after_class.find('.', close_paren);
if (dot_pos == std::string::npos)
return false;
out_class_name = utils::Trim(after_class.substr(0, close_paren));
if (out_class_name.empty())
return false;
out_prefix = utils::Trim(after_class.substr(dot_pos + 1));
return true;
}
bool IsUnitContext(const std::string& line)
{
std::string line_lower = utils::ToLower(line);
std::size_t unit_pos = line_lower.rfind("unit(");
if (unit_pos == std::string::npos)
return false;
std::string after = line.substr(unit_pos + 5);
return after.find(')') == std::string::npos;
}
bool IsClassContext(const std::string& line)
{
std::string line_lower = utils::ToLower(line);
std::size_t class_pos = line_lower.rfind("class(");
if (class_pos == std::string::npos)
return false;
std::string after = line.substr(class_pos + 6);
return after.find(')') == std::string::npos;
}
std::string ExtractPrefixByScan(std::size_t cursor_pos, std::size_t line_start, const std::string& content)
{
std::size_t prefix_start = cursor_pos;
while (prefix_start > line_start)
{
char ch = content[prefix_start - 1];
if (!std::isalnum(static_cast<unsigned char>(ch)) && ch != '_')
break;
--prefix_start;
}
return content.substr(prefix_start, cursor_pos - prefix_start);
}
CompletionContext AnalyzeContext(const protocol::CompletionParams& params, const std::optional<std::string>& document_content)
{
CompletionContext result;
result.uri = params.textDocument.uri;
result.position = params.position;
if (!document_content)
return result;
const std::string& content = *document_content;
std::size_t line_start = 0;
std::size_t current_line = 0;
for (std::size_t i = 0; i < content.length(); ++i)
{
if (current_line == result.position.line)
{
line_start = i;
break;
}
if (content[i] == '\n')
++current_line;
}
std::size_t cursor_pos = std::min(line_start + result.position.character, content.length());
result.line_content = content.substr(line_start, cursor_pos - line_start);
std::string class_name;
std::string method_prefix;
result.is_class_method_context = IsClassMethodContext(result.line_content, class_name, method_prefix);
if (result.is_class_method_context)
{
result.class_name = class_name;
result.prefix = method_prefix;
}
else
{
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.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;
}
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::vector<std::string> CollectVisibleUnits(const language::symbol::SymbolTable* table, const protocol::Position& position, const std::optional<std::string>& content_opt)
{
if (!table)
return {};
std::unordered_set<std::string, utils::IHasher, utils::IEqualTo> units;
auto add_imports = [&units](const auto& imports) {
for (const auto& imp : imports)
units.insert(utils::ToLower(imp.unit_name));
};
// Always include current module name
std::string module_name = GetModuleName(*table);
if (!module_name.empty())
units.insert(utils::ToLower(module_name));
// Gather imports from current symbol scope
if (content_opt)
{
std::uint32_t offset = utils::text_coordinates::ToOffset(position, *content_opt);
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;
if (auto id_opt = table->FindSymbolAt(loc))
{
if (const auto* sym = table->definition(*id_opt))
{
if (const auto* func = sym->As<language::symbol::Function>())
add_imports(func->imports);
else if (const auto* method = sym->As<language::symbol::Method>())
add_imports(method->imports);
else if (const auto* cls = sym->As<language::symbol::Class>())
add_imports(cls->imports);
else if (const auto* unit = sym->As<language::symbol::Unit>())
{
add_imports(unit->interface_imports);
add_imports(unit->implementation_imports);
}
}
}
}
// Unit level imports
for (const auto& wrapper : table->all_definitions())
{
const auto& sym = wrapper.get();
if (sym.kind() == protocol::SymbolKind::Module)
{
const auto* unit = sym.As<language::symbol::Unit>();
if (unit)
{
add_imports(unit->interface_imports);
add_imports(unit->implementation_imports);
}
}
}
std::vector<std::string> result;
result.reserve(units.size());
for (const auto& name : units)
result.push_back(name);
return result;
}
bool IsUnitVisible(const std::unordered_set<std::string, utils::IHasher, utils::IEqualTo>& visible_units, const std::string& unit_name)
{
return visible_units.find(utils::ToLower(unit_name)) != visible_units.end();
}
void AppendKeywordItems(const std::string& prefix, std::vector<SourcedCompletionItem>& out)
{
auto keywords = language::keyword::Repo::Instance().FindByPrefix(utils::ToLower(prefix));
for (const auto& keyword : keywords)
{
protocol::CompletionItem item;
item.label = keyword.keyword;
item.kind = protocol::CompletionItemKind::Keyword;
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = "", .description = SourceTag(CompletionSource::kKeyword) };
out.push_back({ std::move(item), CompletionSource::kKeyword });
}
}
void AppendFunctions(const language::symbol::SymbolTable& table, const std::string& prefix, CompletionSource source, std::vector<SourcedCompletionItem>& out)
{
for (const auto& wrapper : table.all_definitions())
{
const auto& symbol = wrapper.get();
if (symbol.kind() != protocol::SymbolKind::Function)
continue;
if (!prefix.empty() && !utils::IStartsWith(symbol.name(), prefix))
continue;
const auto* func = symbol.As<language::symbol::Function>();
protocol::CompletionItem item;
item.label = symbol.name();
item.kind = ToCompletionItemKind(symbol.kind());
if (func)
{
auto detail = BuildSignature(func->parameters, func->return_type);
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = detail, .description = SourceTag(source) };
}
out.push_back({ std::move(item), source });
}
}
void AppendUnitMembers(const language::symbol::SymbolTable& table,
const std::string& unit_name,
const std::string& member_prefix,
CompletionSource source,
std::vector<SourcedCompletionItem>& out)
{
auto module_name = GetModuleName(table);
if (module_name.empty() || !utils::IEquals(module_name, unit_name))
return;
for (const auto& wrapper : table.all_definitions())
{
const auto& symbol = wrapper.get();
if (symbol.kind() != protocol::SymbolKind::Function &&
symbol.kind() != protocol::SymbolKind::Variable &&
symbol.kind() != protocol::SymbolKind::Constant &&
symbol.kind() != protocol::SymbolKind::Class)
{
continue;
}
if (!member_prefix.empty() && !utils::IStartsWith(symbol.name(), member_prefix))
continue;
protocol::CompletionItem item;
item.label = symbol.name();
item.kind = ToCompletionItemKind(symbol.kind());
if (const auto* func = symbol.As<language::symbol::Function>())
{
auto detail = BuildSignature(func->parameters, func->return_type);
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = detail, .description = SourceTag(source) };
}
else
{
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = "", .description = SourceTag(source) };
}
out.push_back({ std::move(item), source });
}
}
bool HasStaticMethod(const language::symbol::Class& cls, const language::symbol::SymbolTable& table)
{
const auto& scopes = table.scopes().all_scopes();
for (const auto& [_, scope] : scopes)
{
if (scope.kind != language::symbol::ScopeKind::kClass || !scope.owner || *scope.owner != cls.id)
{
continue;
}
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,
std::vector<SourcedCompletionItem>& out,
bool mark_new_context,
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;
const auto* cls = symbol.As<language::symbol::Class>();
if (!cls)
continue;
if (!mark_new_context && !HasStaticMethod(*cls, table))
continue;
protocol::CompletionItem item;
item.label = symbol.name();
item.kind = mark_new_context ? protocol::CompletionItemKind::Constructor : ToCompletionItemKind(symbol.kind());
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = "", .description = SourceTag(source) };
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 });
}
}
void AppendUnits(const language::symbol::SymbolTable& table,
const std::string& prefix,
CompletionSource source,
std::vector<SourcedCompletionItem>& out,
const std::unordered_set<std::string, utils::IHasher, utils::IEqualTo>* allowed_units = nullptr)
{
auto module_name = GetModuleName(table);
if (!module_name.empty() && allowed_units && !IsUnitVisible(*allowed_units, module_name))
return;
for (const auto& wrapper : table.all_definitions())
{
const auto& symbol = wrapper.get();
if (symbol.kind() != protocol::SymbolKind::Module)
continue;
if (!prefix.empty() && !utils::IStartsWith(symbol.name(), prefix))
continue;
protocol::CompletionItem item;
item.label = symbol.name();
item.kind = ToCompletionItemKind(symbol.kind());
item.labelDetails = protocol::CompletionItemLabelDetails{ .detail = "", .description = SourceTag(source) };
item.data = protocol::LSPAny("unit_context");
out.push_back({ std::move(item), source });
}
}
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;
}
struct DotAccessContext
{
bool is_unit_call = false;
std::string base_name;
std::string member_prefix;
};
struct QualifiedPrefix
{
std::string unit_name;
std::string member_prefix;
};
std::optional<QualifiedPrefix> ParseUnitQualifiedPrefix(const std::string& text)
{
std::string trimmed = utils::Trim(text);
if (trimmed.empty())
{
return std::nullopt;
}
std::string lower = utils::ToLower(trimmed);
// unit(UnitA).Prefix
if (lower.starts_with("unit("))
{
std::string after_unit = trimmed.substr(5);
std::size_t close_paren = after_unit.find(')');
if (close_paren == std::string::npos)
{
return std::nullopt;
}
std::string unit_name = utils::Trim(after_unit.substr(0, close_paren));
std::size_t dot_pos = after_unit.find('.', close_paren);
std::string member_prefix;
if (dot_pos != std::string::npos && dot_pos + 1 <= after_unit.size())
{
member_prefix = utils::Trim(after_unit.substr(dot_pos + 1));
}
if (unit_name.empty())
{
return std::nullopt;
}
return QualifiedPrefix{
.unit_name = std::move(unit_name),
.member_prefix = std::move(member_prefix),
};
}
// UnitA.Prefix
std::size_t dot_pos = trimmed.find_last_of('.');
if (dot_pos != std::string::npos)
{
std::string unit_name = utils::Trim(trimmed.substr(0, dot_pos));
std::string member_prefix;
if (dot_pos + 1 <= trimmed.size())
{
member_prefix = utils::Trim(trimmed.substr(dot_pos + 1));
}
if (unit_name.empty())
{
return std::nullopt;
}
return QualifiedPrefix{
.unit_name = std::move(unit_name),
.member_prefix = std::move(member_prefix),
};
}
return std::nullopt;
}
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;
}
std::vector<std::string> CollectUnitSearchOrder(const language::symbol::SymbolTable& table,
const protocol::Position& position,
const std::string& content)
{
std::vector<std::string> imports;
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 add_imports = [&imports](const auto& vec) {
imports.reserve(imports.size() + vec.size());
for (const auto& imp : vec)
{
imports.push_back(imp.unit_name);
}
};
if (auto id_opt = table.FindSymbolAt(loc))
{
if (const auto* sym = table.definition(*id_opt))
{
if (const auto* func = sym->As<language::symbol::Function>())
add_imports(func->imports);
else if (const auto* method = sym->As<language::symbol::Method>())
add_imports(method->imports);
else if (const auto* cls = sym->As<language::symbol::Class>())
add_imports(cls->imports);
else if (const auto* unit = sym->As<language::symbol::Unit>())
{
add_imports(unit->interface_imports);
add_imports(unit->implementation_imports);
}
}
}
if (imports.empty())
{
for (const auto& wrapper : table.all_definitions())
{
const auto& sym = wrapper.get();
if (sym.kind() == protocol::SymbolKind::Module)
{
if (const auto* unit = sym.As<language::symbol::Unit>())
{
add_imports(unit->interface_imports);
add_imports(unit->implementation_imports);
}
break;
}
}
}
std::vector<std::string> order;
std::unordered_set<std::string, utils::IHasher, utils::IEqualTo> seen;
std::string module_name = GetModuleName(table);
if (!module_name.empty())
{
order.push_back(module_name);
seen.insert(module_name);
}
for (auto it = imports.rbegin(); it != imports.rend(); ++it)
{
if (it->empty())
{
continue;
}
if (seen.find(*it) != seen.end())
{
continue;
}
order.push_back(*it);
seen.insert(*it);
}
return order;
}
void AppendClassMethods(const language::symbol::SymbolTable& table, const language::symbol::Class& cls, const std::string& prefix, CompletionSource source, std::vector<SourcedCompletionItem>& out)
{
auto scope_id = FindScopeOwnedBy(table, language::symbol::ScopeKind::kClass, cls.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_symbol = table.definition(id);
if (!member_symbol || member_symbol->kind() != protocol::SymbolKind::Method)
continue;
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 = 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 });
}
}
std::vector<SourcedCompletionItem> FilterAndSort(const std::vector<SourcedCompletionItem>& items, const std::string& prefix)
{
std::vector<const SourcedCompletionItem*> unique_items;
std::set<std::string> seen_labels;
for (const auto& item : items)
{
if (seen_labels.count(item.item.label) > 0)
continue;
seen_labels.insert(item.item.label);
if (!prefix.empty() && !utils::IStartsWith(item.item.label, prefix))
continue;
unique_items.push_back(&item);
}
std::sort(
unique_items.begin(),
unique_items.end(),
[&prefix](const SourcedCompletionItem* a, const SourcedCompletionItem* b) {
std::string a_lower = utils::ToLower(a->item.label);
std::string b_lower = utils::ToLower(b->item.label);
std::string prefix_lower = utils::ToLower(prefix);
bool a_exact = (a_lower == prefix_lower);
bool b_exact = (b_lower == prefix_lower);
if (a_exact != b_exact)
return a_exact;
if (!prefix.empty())
{
int a_score = static_cast<int>(prefix.length()) * 100 / static_cast<int>(a->item.label.length());
int b_score = static_cast<int>(prefix.length()) * 100 / static_cast<int>(b->item.label.length());
if (a_score != b_score)
return a_score > b_score;
}
if (a->source != b->source)
{
auto priority = [](CompletionSource src) -> int {
switch (src)
{
case CompletionSource::kEditing:
return 0;
case CompletionSource::kWorkspace:
return 1;
case CompletionSource::kSystem:
return 2;
case CompletionSource::kKeyword:
default:
return 3;
}
};
return priority(a->source) < priority(b->source);
}
return a_lower < b_lower;
});
std::vector<SourcedCompletionItem> result;
result.reserve(unique_items.size());
for (const auto* item : unique_items)
result.push_back(*item);
return result;
}
protocol::CompletionList BuildCompletionList(const protocol::CompletionParams& params, ExecutionContext& execution_context)
{
protocol::CompletionList list;
list.isIncomplete = false;
auto& manager_hub = execution_context.GetManagerHub();
const auto content = manager_hub.documents().GetContent(params.textDocument.uri);
auto context = AnalyzeContext(params, content);
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();
std::unordered_set<std::string, utils::IHasher, utils::IEqualTo> visible_units_set;
if (editing_table)
{
auto visible_units = CollectVisibleUnits(editing_table, params.position, content);
for (auto& u : visible_units)
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())
{
std::string lower_prefix = utils::ToLower(context.prefix);
if (lower_prefix.starts_with("unit("))
{
auto after = context.prefix.substr(5);
auto close = after.find(')');
auto dot = after.find('.');
std::size_t name_end = std::min(
close == std::string::npos ? after.size() : close,
dot == std::string::npos ? after.size() : dot);
context.unit_name = utils::Trim(after.substr(0, name_end));
if (!context.unit_name.empty())
{
context.is_unit_scoped_new = true;
if (dot != std::string::npos && dot + 1 < after.size())
context.prefix = utils::Trim(after.substr(dot + 1));
else
context.prefix = "";
}
}
else
{
auto dot = context.prefix.find_last_of('.');
if (dot != std::string::npos)
{
auto alias = utils::Trim(context.prefix.substr(0, dot));
if (!alias.empty())
{
context.is_unit_scoped_new = true;
context.unit_name = alias;
context.prefix = utils::Trim(context.prefix.substr(dot + 1));
}
}
}
}
if (context.is_class_method_context)
{
bool found = false;
if (editing_table)
{
if (auto cls_opt = FindClassSymbol(*editing_table, context.class_name))
{
const auto* cls = cls_opt.value()->As<language::symbol::Class>();
if (cls)
{
AppendClassMethods(*editing_table, *cls, context.prefix, CompletionSource::kEditing, collected);
found = true;
}
}
}
if (!found)
{
for (const auto* table : workspace_tables)
{
if (auto cls_opt = FindClassSymbol(*table, context.class_name))
{
const auto* cls = cls_opt.value()->As<language::symbol::Class>();
if (cls)
AppendClassMethods(*table, *cls, context.prefix, CompletionSource::kWorkspace, collected);
found = true;
break;
}
}
}
if (!found)
{
for (const auto* table : system_tables)
{
if (auto cls_opt = FindClassSymbol(*table, context.class_name))
{
const auto* cls = cls_opt.value()->As<language::symbol::Class>();
if (cls)
AppendClassMethods(*table, *cls, context.prefix, CompletionSource::kSystem, collected);
break;
}
}
}
}
else if (context.is_unit_member_context)
{
if (context.unit_name.empty() || !IsUnitVisible(visible_units_set, context.unit_name))
{
// not visible -> no unit member suggestions
}
else
{
if (editing_table)
AppendUnitMembers(*editing_table, context.unit_name, context.member_prefix, CompletionSource::kEditing, collected);
for (const auto* table : workspace_tables)
AppendUnitMembers(*table, context.unit_name, context.member_prefix, CompletionSource::kWorkspace, collected);
for (const auto* table : system_tables)
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))
{
std::string type_text = utils::Trim(*type_name);
std::string class_name = type_text;
std::vector<std::string> unit_candidates;
if (auto qualified = ParseUnitQualifiedPrefix(type_text))
{
if (!qualified->unit_name.empty() && !qualified->member_prefix.empty())
{
unit_candidates.push_back(qualified->unit_name);
class_name = qualified->member_prefix;
}
}
if (!class_name.empty())
{
if (unit_candidates.empty())
{
unit_candidates = CollectUnitSearchOrder(*editing_table, params.position, *content);
}
bool found_class = false;
for (const auto& unit_name : unit_candidates)
{
if (found_class)
break;
auto try_table = [&](const language::symbol::SymbolTable& table, CompletionSource source) {
if (!utils::IEquals(GetModuleName(table), unit_name))
{
return;
}
auto cls_opt = FindClassSymbol(table, class_name);
if (!cls_opt)
{
return;
}
AppendInstanceMembers(table, (*cls_opt)->id(), context.member_prefix, source, collected);
found_class = true;
};
try_table(*editing_table, CompletionSource::kEditing);
for (const auto* table : workspace_tables)
try_table(*table, CompletionSource::kWorkspace);
for (const auto* table : system_tables)
try_table(*table, CompletionSource::kSystem);
}
(void)found_class;
}
}
}
}
else if (context.is_new_context)
{
if (context.is_unit_scoped_new && !context.unit_name.empty())
{
if (editing_table)
AppendClasses(*editing_table, context.prefix, CompletionSource::kEditing, collected, true, context.unit_name);
for (const auto* table : workspace_tables)
AppendClasses(*table, context.prefix, CompletionSource::kWorkspace, collected, true, context.unit_name);
for (const auto* table : system_tables)
AppendClasses(*table, context.prefix, CompletionSource::kSystem, collected, true, context.unit_name);
}
else
{
if (editing_table && content)
{
auto unit_order = CollectUnitSearchOrder(*editing_table, params.position, *content);
if (!unit_order.empty())
{
for (const auto& unit_name : unit_order)
{
AppendClasses(*editing_table, context.prefix, CompletionSource::kEditing, collected, true, unit_name);
for (const auto* table : workspace_tables)
AppendClasses(*table, context.prefix, CompletionSource::kWorkspace, collected, true, unit_name);
for (const auto* table : system_tables)
AppendClasses(*table, context.prefix, CompletionSource::kSystem, collected, true, unit_name);
}
}
else
{
AppendClasses(*editing_table, context.prefix, CompletionSource::kEditing, collected, true);
for (const auto* table : workspace_tables)
AppendClasses(*table, context.prefix, CompletionSource::kWorkspace, collected, true);
for (const auto* table : system_tables)
AppendClasses(*table, context.prefix, CompletionSource::kSystem, collected, true);
}
}
else
{
if (editing_table)
AppendClasses(*editing_table, context.prefix, CompletionSource::kEditing, collected, true);
for (const auto* table : workspace_tables)
AppendClasses(*table, context.prefix, CompletionSource::kWorkspace, collected, true);
for (const auto* table : system_tables)
AppendClasses(*table, context.prefix, CompletionSource::kSystem, collected, true);
}
}
}
else if (context.is_createobject_context)
{
if (auto qualified = ParseUnitQualifiedPrefix(context.prefix))
{
if (editing_table)
AppendCreateObjectClasses(*editing_table, qualified->member_prefix, CompletionSource::kEditing, collected, context.createobject_has_open_quote, context.createobject_quote, qualified->unit_name);
for (const auto* table : workspace_tables)
AppendCreateObjectClasses(*table, qualified->member_prefix, CompletionSource::kWorkspace, collected, context.createobject_has_open_quote, context.createobject_quote, qualified->unit_name);
for (const auto* table : system_tables)
AppendCreateObjectClasses(*table, qualified->member_prefix, CompletionSource::kSystem, collected, context.createobject_has_open_quote, context.createobject_quote, qualified->unit_name);
}
else if (editing_table && content)
{
auto unit_order = CollectUnitSearchOrder(*editing_table, params.position, *content);
if (!unit_order.empty())
{
for (const auto& unit_name : unit_order)
{
AppendCreateObjectClasses(*editing_table, context.prefix, CompletionSource::kEditing, collected, context.createobject_has_open_quote, context.createobject_quote, unit_name);
for (const auto* table : workspace_tables)
AppendCreateObjectClasses(*table, context.prefix, CompletionSource::kWorkspace, collected, context.createobject_has_open_quote, context.createobject_quote, unit_name);
for (const auto* table : system_tables)
AppendCreateObjectClasses(*table, context.prefix, CompletionSource::kSystem, collected, context.createobject_has_open_quote, context.createobject_quote, unit_name);
}
}
else
{
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 (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())
{
if (editing_table)
AppendUnits(*editing_table, context.prefix, CompletionSource::kEditing, collected, &visible_units_set);
for (const auto* table : workspace_tables)
AppendUnits(*table, context.prefix, CompletionSource::kWorkspace, collected, &visible_units_set);
for (const auto* table : system_tables)
AppendUnits(*table, context.prefix, CompletionSource::kSystem, collected, &visible_units_set);
}
}
else if (context.is_class_context)
{
if (editing_table)
AppendClasses(*editing_table, context.prefix, CompletionSource::kEditing, collected, false);
for (const auto* table : workspace_tables)
AppendClasses(*table, context.prefix, CompletionSource::kWorkspace, collected, false);
for (const auto* table : system_tables)
AppendClasses(*table, context.prefix, CompletionSource::kSystem, collected, false);
}
else
{
AppendKeywordItems(context.prefix, collected);
if (editing_table)
AppendFunctions(*editing_table, context.prefix, CompletionSource::kEditing, collected);
for (const auto* table : workspace_tables)
AppendFunctions(*table, context.prefix, CompletionSource::kWorkspace, collected);
for (const auto* table : system_tables)
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;
}
std::string filter_prefix = context.prefix;
if (context.is_createobject_context)
{
if (auto qualified = ParseUnitQualifiedPrefix(context.prefix))
{
filter_prefix = qualified->member_prefix;
}
}
auto filtered = FilterAndSort(collected, filter_prefix);
list.items.reserve(filtered.size());
for (auto& item : filtered)
list.items.push_back(std::move(item.item));
return list;
}
}
std::string Completion::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& execution_context)
{
spdlog::debug("{}: Processing completion request", GetProviderName());
if (!request.params.has_value())
{
spdlog::warn("{}: Missing params in request", GetProviderName());
return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params");
}
const auto params = transform::FromLSPAny.template operator()<protocol::CompletionParams>(request.params.value());
auto completion_list = BuildCompletionList(params, execution_context);
protocol::ResponseMessage response;
response.id = request.id;
response.result = transform::ToLSPAny(completion_list);
auto json = transform::Serialize(response);
if (!json)
return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response");
return *json;
}
}