mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-23 19:51:05 -04:00
feat: add support code completion via acp
This commit is contained in:
282
sources/context/CompletionContextEnricher.cpp
Normal file
282
sources/context/CompletionContextEnricher.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "CompletionContextEnricher.hpp"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
|
||||
#include <cplusplus/Literals.h>
|
||||
#include <cplusplus/Names.h>
|
||||
#include <cplusplus/Overview.h>
|
||||
#include <cplusplus/Scope.h>
|
||||
#include <cplusplus/Symbols.h>
|
||||
#include <cppeditor/cppmodelmanager.h>
|
||||
#include <qmljs/qmljsdocument.h>
|
||||
#include <qmljs/qmljsmodelmanagerinterface.h>
|
||||
#include <utils/filepath.h>
|
||||
|
||||
#include "ProgrammingLanguage.hpp"
|
||||
#include "TokenUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kMaxPrefixIdentifiers = 40;
|
||||
constexpr int kMaxEnclosingMembers = 40;
|
||||
constexpr int kMaxReferencedDeclarations = 60;
|
||||
constexpr int kMaxIncludedDocuments = 30;
|
||||
constexpr int kMaxSiblingComponents = 60;
|
||||
constexpr int kMaxVisitedSymbols = 5000;
|
||||
constexpr int kPrefixScanWindow = 4096;
|
||||
|
||||
const QSet<QString> &cppKeywords()
|
||||
{
|
||||
static const QSet<QString> keywords = {
|
||||
"alignas", "alignof", "asm", "auto", "bool", "break",
|
||||
"case", "catch", "char", "class", "const", "constexpr",
|
||||
"consteval", "constinit","continue", "decltype", "default", "delete",
|
||||
"do", "double", "else", "enum", "explicit", "export",
|
||||
"extern", "false", "float", "for", "friend", "goto",
|
||||
"if", "inline", "int", "long", "mutable", "namespace",
|
||||
"new", "noexcept", "nullptr", "operator", "private", "protected",
|
||||
"public", "register", "requires", "return", "short", "signed",
|
||||
"sizeof", "static", "struct", "switch", "template", "this",
|
||||
"throw", "true", "try", "typedef", "typeid", "typename",
|
||||
"union", "unsigned", "using", "virtual", "void", "volatile",
|
||||
"while", "override", "final", "co_await", "co_return", "co_yield"};
|
||||
return keywords;
|
||||
}
|
||||
|
||||
QString prettySymbol(const CPlusPlus::Overview &overview, CPlusPlus::Symbol *symbol)
|
||||
{
|
||||
return overview.prettyType(symbol->type(), overview.prettyName(symbol->name()));
|
||||
}
|
||||
|
||||
QString cppEnclosingScopeSection(
|
||||
const CPlusPlus::Document::Ptr &document,
|
||||
const CPlusPlus::Overview &overview,
|
||||
int line,
|
||||
int column)
|
||||
{
|
||||
CPlusPlus::Scope *scope = document->scopeAt(line + 1, column + 1);
|
||||
for (CPlusPlus::Scope *current = scope; current; current = current->enclosingScope()) {
|
||||
CPlusPlus::Class *klass = current->asClass();
|
||||
if (!klass || !klass->name())
|
||||
continue;
|
||||
|
||||
QStringList members;
|
||||
const int memberCount = qMin(klass->memberCount(), kMaxEnclosingMembers);
|
||||
for (int i = 0; i < memberCount; ++i) {
|
||||
CPlusPlus::Symbol *member = klass->memberAt(i);
|
||||
if (!member || !member->name())
|
||||
continue;
|
||||
members.append(" " + prettySymbol(overview, member));
|
||||
}
|
||||
|
||||
if (members.isEmpty())
|
||||
continue;
|
||||
|
||||
return QString("Enclosing class %1:\n%2")
|
||||
.arg(overview.prettyName(klass->name()), members.join('\n'));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QString cppReferencedDeclarationsSection(
|
||||
const CPlusPlus::Snapshot &snapshot,
|
||||
const CPlusPlus::Document::Ptr &document,
|
||||
const CPlusPlus::Overview &overview,
|
||||
const QString &prefix)
|
||||
{
|
||||
const QStringList identifiers
|
||||
= identifiersNearCursor(prefix.right(kPrefixScanWindow), kMaxPrefixIdentifiers);
|
||||
if (identifiers.isEmpty())
|
||||
return {};
|
||||
|
||||
QSet<QByteArray> wanted;
|
||||
for (const QString &identifier : identifiers)
|
||||
wanted.insert(identifier.toUtf8());
|
||||
|
||||
QList<CPlusPlus::Document::Ptr> documents{document};
|
||||
const Utils::FilePaths includes = document->includedFiles();
|
||||
for (const Utils::FilePath &include : includes) {
|
||||
if (documents.size() > kMaxIncludedDocuments)
|
||||
break;
|
||||
if (auto included = snapshot.document(include))
|
||||
documents.append(included);
|
||||
}
|
||||
|
||||
QStringList declarations;
|
||||
QSet<QString> seen;
|
||||
int visited = 0;
|
||||
|
||||
auto collect = [&](auto self, CPlusPlus::Scope *scope) -> void {
|
||||
if (!scope || declarations.size() >= kMaxReferencedDeclarations
|
||||
|| visited >= kMaxVisitedSymbols)
|
||||
return;
|
||||
for (int i = 0; i < scope->memberCount(); ++i) {
|
||||
if (declarations.size() >= kMaxReferencedDeclarations
|
||||
|| ++visited >= kMaxVisitedSymbols)
|
||||
return;
|
||||
CPlusPlus::Symbol *symbol = scope->memberAt(i);
|
||||
if (!symbol || !symbol->name())
|
||||
continue;
|
||||
if (const CPlusPlus::Identifier *nameId = symbol->name()->asNameId()) {
|
||||
const QByteArray key
|
||||
= QByteArray::fromRawData(nameId->chars(), int(nameId->size()));
|
||||
if (wanted.contains(key)) {
|
||||
const QString pretty = " " + prettySymbol(overview, symbol);
|
||||
if (!seen.contains(pretty)) {
|
||||
seen.insert(pretty);
|
||||
declarations.append(pretty);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (symbol->asFunction() || symbol->asBlock())
|
||||
continue;
|
||||
if (auto nested = symbol->asScope())
|
||||
self(self, nested);
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto &doc : std::as_const(documents)) {
|
||||
if (doc->globalNamespace())
|
||||
collect(collect, doc->globalNamespace());
|
||||
}
|
||||
|
||||
if (declarations.isEmpty())
|
||||
return {};
|
||||
|
||||
return QString("Declarations referenced near the cursor:\n%1").arg(declarations.join('\n'));
|
||||
}
|
||||
|
||||
QStringList cppEnrichment(const QString &filePath, const QString &prefix, int line, int column)
|
||||
{
|
||||
auto *modelManager = CppEditor::CppModelManager::instance();
|
||||
if (!modelManager)
|
||||
return {};
|
||||
|
||||
const CPlusPlus::Snapshot snapshot = modelManager->snapshot();
|
||||
const CPlusPlus::Document::Ptr document
|
||||
= snapshot.document(Utils::FilePath::fromString(filePath));
|
||||
if (!document || !document->globalNamespace())
|
||||
return {};
|
||||
|
||||
CPlusPlus::Overview overview;
|
||||
overview.showReturnTypes = true;
|
||||
overview.showArgumentNames = true;
|
||||
|
||||
QStringList sections;
|
||||
const QString enclosing = cppEnclosingScopeSection(document, overview, line, column);
|
||||
if (!enclosing.isEmpty())
|
||||
sections.append(enclosing);
|
||||
const QString referenced
|
||||
= cppReferencedDeclarationsSection(snapshot, document, overview, prefix);
|
||||
if (!referenced.isEmpty())
|
||||
sections.append(referenced);
|
||||
return sections;
|
||||
}
|
||||
|
||||
QStringList qmlEnrichment(const QString &filePath)
|
||||
{
|
||||
auto *modelManager = QmlJS::ModelManagerInterface::instance();
|
||||
if (!modelManager)
|
||||
return {};
|
||||
|
||||
const QmlJS::Snapshot snapshot = modelManager->snapshot();
|
||||
const Utils::FilePath path = Utils::FilePath::fromString(filePath);
|
||||
const QmlJS::Document::Ptr document = snapshot.document(path);
|
||||
const Utils::FilePath directory = document ? document->path() : path.parentDir();
|
||||
|
||||
QStringList components;
|
||||
const QList<QmlJS::Document::Ptr> siblings = snapshot.documentsInDirectory(directory);
|
||||
for (const auto &sibling : siblings) {
|
||||
if (components.size() >= kMaxSiblingComponents)
|
||||
break;
|
||||
if (!sibling || sibling->fileName() == path)
|
||||
continue;
|
||||
const QString component = sibling->componentName();
|
||||
if (!component.isEmpty() && !components.contains(component))
|
||||
components.append(component);
|
||||
}
|
||||
|
||||
if (components.isEmpty())
|
||||
return {};
|
||||
|
||||
components.sort();
|
||||
return QStringList{
|
||||
QString("QML components available in this directory: %1").arg(components.join(", "))};
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
QString SemanticContextEnricher::enrichmentFor(
|
||||
const DocumentInfo &documentInfo, const QString &prefix, int line, int column, int maxTokens)
|
||||
{
|
||||
const ProgrammingLanguage language
|
||||
= ProgrammingLanguageUtils::fromMimeType(documentInfo.mimeType);
|
||||
|
||||
QStringList sections;
|
||||
switch (language) {
|
||||
case ProgrammingLanguage::Cpp:
|
||||
sections = cppEnrichment(documentInfo.filePath, prefix, line, column);
|
||||
break;
|
||||
case ProgrammingLanguage::QML:
|
||||
sections = qmlEnrichment(documentInfo.filePath);
|
||||
break;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
const QString clamped = clampSectionsToTokenBudget(sections, maxTokens);
|
||||
if (clamped.isEmpty())
|
||||
return {};
|
||||
|
||||
return QString("\n\n# Project code model context (may lag unsaved edits)\n%1\n").arg(clamped);
|
||||
}
|
||||
|
||||
QString clampSectionsToTokenBudget(const QStringList §ions, int maxTokens)
|
||||
{
|
||||
QStringList kept;
|
||||
int remaining = maxTokens;
|
||||
for (const QString §ion : sections) {
|
||||
if (section.isEmpty())
|
||||
continue;
|
||||
const int cost = TokenUtils::estimateTokens(section);
|
||||
if (cost > remaining)
|
||||
continue;
|
||||
kept.append(section);
|
||||
remaining -= cost;
|
||||
}
|
||||
return kept.join("\n\n");
|
||||
}
|
||||
|
||||
QStringList identifiersNearCursor(const QString &prefix, int maxIdentifiers)
|
||||
{
|
||||
static const QRegularExpression identifierRegex(
|
||||
QStringLiteral("[A-Za-z_][A-Za-z0-9_]{2,}"));
|
||||
|
||||
QStringList result;
|
||||
QSet<QString> seen;
|
||||
|
||||
QRegularExpressionMatchIterator it = identifierRegex.globalMatch(prefix);
|
||||
QStringList all;
|
||||
while (it.hasNext())
|
||||
all.append(it.next().captured());
|
||||
|
||||
for (auto identifier = all.crbegin(); identifier != all.crend(); ++identifier) {
|
||||
if (result.size() >= maxIdentifiers)
|
||||
break;
|
||||
if (seen.contains(*identifier) || cppKeywords().contains(*identifier))
|
||||
continue;
|
||||
seen.insert(*identifier);
|
||||
result.append(*identifier);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
Reference in New Issue
Block a user