refactor: Move to agent architecture

This commit is contained in:
Petr Mironychev
2026-05-30 14:50:49 +02:00
parent 34ce787320
commit ccc2ec2e80
364 changed files with 10801 additions and 19020 deletions

View File

@@ -1,7 +1,10 @@
add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp
EnvBlockFormatter.hpp EnvBlockFormatter.cpp
ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp
IProjectScanner.hpp
ProjectScannerQtCreator.hpp ProjectScannerQtCreator.cpp
ContentFile.hpp
DocumentReaderQtCreator.hpp
IDocumentReader.hpp
@@ -21,7 +24,7 @@ target_link_libraries(Context
QtCreator::Utils
QtCreator::ProjectExplorer
PRIVATE
PluginLLMCore
Common
QodeAssistSettings
)

View File

@@ -282,175 +282,6 @@ ChangesManager::FileEdit ChangesManager::getFileEdit(const QString &editId) cons
return m_fileEdits.value(editId);
}
QList<ChangesManager::FileEdit> ChangesManager::getPendingEdits() const
{
QMutexLocker locker(&m_mutex);
QList<FileEdit> pendingEdits;
for (const auto &edit : m_fileEdits.values()) {
if (edit.status == Pending) {
pendingEdits.append(edit);
}
}
return pendingEdits;
}
bool ChangesManager::performFileEdit(
const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg)
{
auto setError = [errorMsg](const QString &msg) {
if (errorMsg) *errorMsg = msg;
};
auto editors = Core::EditorManager::visibleEditors();
for (auto *editor : editors) {
if (!editor || !editor->document()) {
continue;
}
QString editorPath = editor->document()->filePath().toFSPathString();
if (editorPath == filePath) {
QByteArray contentBytes = editor->document()->contents();
QString currentContent = QString::fromUtf8(contentBytes);
if (oldContent.isEmpty()) {
if (auto *textEditor
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
QTextDocument *doc = textEditor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
cursor.movePosition(QTextCursor::End);
cursor.insertText(newContent);
cursor.endEditBlock();
LOG_MESSAGE(QString("Appended to open editor: %1").arg(filePath));
setError("Applied successfully (appended to end of file)");
return true;
}
}
int matchPos = currentContent.indexOf(oldContent);
if (matchPos != -1) {
if (auto *textEditor
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
QTextDocument *doc = textEditor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
cursor.setPosition(matchPos);
cursor.setPosition(matchPos + oldContent.length(), QTextCursor::KeepAnchor);
cursor.removeSelectedText();
cursor.insertText(newContent);
cursor.endEditBlock();
LOG_MESSAGE(QString("Updated open editor (exact match): %1").arg(filePath));
setError("Applied successfully (exact match)");
return true;
}
} else {
double similarity = 0.0;
QString matchedContent = findBestMatch(currentContent, oldContent, 0.82, &similarity);
if (!matchedContent.isEmpty()) {
matchPos = currentContent.indexOf(matchedContent);
if (matchPos != -1) {
if (auto *textEditor
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
QTextDocument *doc = textEditor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
cursor.setPosition(matchPos);
cursor.setPosition(matchPos + matchedContent.length(), QTextCursor::KeepAnchor);
cursor.removeSelectedText();
cursor.insertText(newContent);
cursor.endEditBlock();
LOG_MESSAGE(QString("Updated open editor (fuzzy match %1%%): %2")
.arg(qRound(similarity * 100)).arg(filePath));
setError(QString("Applied with fuzzy match (%1%% similarity)").arg(qRound(similarity * 100)));
return true;
}
}
}
LOG_MESSAGE(QString("Old content not found in open editor (best similarity: %1%%): %2")
.arg(qRound(similarity * 100)).arg(filePath));
setError(QString("Content not found. Best match: %1%% (threshold: 82%%). "
"File may have changed.").arg(qRound(similarity * 100)));
return false;
}
}
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString msg = QString("Cannot open file: %1").arg(file.errorString());
LOG_MESSAGE(QString("Failed to open file for reading: %1 - %2").arg(filePath, file.errorString()));
setError(msg);
return false;
}
QString currentContent = QString::fromUtf8(file.readAll());
file.close();
QString updatedContent;
if (oldContent.isEmpty()) {
updatedContent = currentContent + newContent;
LOG_MESSAGE(QString("Appending to file: %1").arg(filePath));
setError("Applied successfully (appended to end of file)");
}
else if (currentContent.contains(oldContent)) {
int matchPos = currentContent.indexOf(oldContent);
updatedContent = currentContent.left(matchPos)
+ newContent
+ currentContent.mid(matchPos + oldContent.length());
LOG_MESSAGE(QString("Using exact match for file update: %1 at position %2")
.arg(filePath).arg(matchPos));
setError("Applied successfully (exact match)");
} else {
double similarity = 0.0;
QString matchedContent = findBestMatch(currentContent, oldContent, 0.82, &similarity);
if (!matchedContent.isEmpty()) {
int matchPos = currentContent.indexOf(matchedContent);
if (matchPos == -1) {
QString msg = "Internal error: matched content not found in file";
LOG_MESSAGE(QString("Internal error: matched content disappeared: %1").arg(filePath));
setError(msg);
return false;
}
updatedContent = currentContent.left(matchPos)
+ newContent
+ currentContent.mid(matchPos + matchedContent.length());
LOG_MESSAGE(QString("Using fuzzy match (%1%%) for file update: %2 at position %3")
.arg(qRound(similarity * 100)).arg(filePath).arg(matchPos));
setError(QString("Applied with fuzzy match (%1%% similarity)").arg(qRound(similarity * 100)));
} else {
QString msg = QString("Content not found. Best match: %1%% (threshold: 82%%). "
"File may have changed.").arg(qRound(similarity * 100));
LOG_MESSAGE(QString("Old content not found in file (best similarity: %1%%): %2")
.arg(qRound(similarity * 100)).arg(filePath));
setError(msg);
return false;
}
}
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
QString msg = QString("Cannot write file: %1").arg(file.errorString());
LOG_MESSAGE(QString("Failed to open file for writing: %1 - %2").arg(filePath, file.errorString()));
setError(msg);
return false;
}
QTextStream out(&file);
out << updatedContent;
file.close();
LOG_MESSAGE(QString("File updated: %1").arg(filePath));
return true;
}
int ChangesManager::levenshteinDistance(const QString &s1, const QString &s2) const
{
const int len1 = s1.length();
@@ -1112,138 +943,6 @@ QString ChangesManager::readFileContent(const QString &filePath) const
return content;
}
bool ChangesManager::performFileEditWithDiff(
const QString &filePath,
const DiffInfo &diffInfo,
bool reverse,
QString *errorMsg)
{
LOG_MESSAGE(QString("=== performFileEditWithDiff: %1 (reverse: %2) ===")
.arg(filePath).arg(reverse ? "yes" : "no"));
auto setError = [errorMsg](const QString &msg) {
if (errorMsg) *errorMsg = msg;
};
auto editors = Core::EditorManager::visibleEditors();
LOG_MESSAGE(QString(" Checking %1 visible editor(s)").arg(editors.size()));
for (auto *editor : editors) {
if (!editor || !editor->document()) {
continue;
}
QString editorPath = editor->document()->filePath().toFSPathString();
if (editorPath == filePath) {
LOG_MESSAGE(QString(" Found open editor for: %1").arg(filePath));
QByteArray contentBytes = editor->document()->contents();
QString currentContent = QString::fromUtf8(contentBytes);
LOG_MESSAGE(QString(" Current content size: %1 bytes").arg(currentContent.size()));
QString modifiedContent = currentContent;
QString diffErrorMsg;
bool diffSuccess = applyDiffToContent(modifiedContent, diffInfo, reverse, &diffErrorMsg);
if (!diffSuccess) {
LOG_MESSAGE(QString(" Failed to apply diff: %1").arg(diffErrorMsg));
setError(diffErrorMsg);
LOG_MESSAGE(" Attempting fallback to old content-based method...");
QString oldContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
QString newContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
return performFileEdit(filePath, oldContent, newContent, errorMsg);
}
if (auto *textEditor = qobject_cast<TextEditor::TextDocument *>(editor->document())) {
QTextDocument *doc = textEditor->document();
LOG_MESSAGE(" Applying changes to text editor document...");
if (!doc) {
LOG_MESSAGE(" Document is invalid");
setError("Document pointer is null");
return false;
}
try {
QTextCursor cursor(doc);
if (cursor.isNull()) {
LOG_MESSAGE(" Cursor is invalid");
setError("Cannot create text cursor");
return false;
}
cursor.beginEditBlock();
cursor.select(QTextCursor::Document);
cursor.removeSelectedText();
cursor.insertText(modifiedContent);
cursor.endEditBlock();
LOG_MESSAGE(QString(" ✓ Successfully applied diff to open editor: %1").arg(filePath));
setError(diffErrorMsg);
return true;
} catch (...) {
LOG_MESSAGE(" Exception during document modification");
setError("Exception during document modification");
return false;
}
}
}
}
LOG_MESSAGE(" File not open in editor, modifying file directly...");
LOG_MESSAGE(" Note: Undo (Ctrl+Z) will not be available for this file until it is opened");
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString msg = QString("Cannot open file: %1").arg(file.errorString());
LOG_MESSAGE(QString(" Failed to open file for reading: %1 - %2")
.arg(filePath, file.errorString()));
setError(msg);
return false;
}
QString currentContent = QString::fromUtf8(file.readAll());
file.close();
LOG_MESSAGE(QString(" File read successfully (%1 bytes)").arg(currentContent.size()));
QString modifiedContent = currentContent;
QString diffErrorMsg;
bool diffSuccess = applyDiffToContent(modifiedContent, diffInfo, reverse, &diffErrorMsg);
if (!diffSuccess) {
LOG_MESSAGE(QString(" Failed to apply diff to file: %1").arg(diffErrorMsg));
setError(diffErrorMsg);
LOG_MESSAGE(" Attempting fallback to old content-based method...");
QString oldContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
QString newContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
return performFileEdit(filePath, oldContent, newContent, errorMsg);
}
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
QString msg = QString("Cannot write file: %1").arg(file.errorString());
LOG_MESSAGE(QString(" Failed to open file for writing: %1 - %2")
.arg(filePath, file.errorString()));
setError(msg);
return false;
}
QTextStream out(&file);
out << modifiedContent;
file.close();
LOG_MESSAGE(QString(" ✓ Successfully wrote modified content to file: %1").arg(filePath));
setError(diffErrorMsg);
return true;
}
ChangesManager::DiffInfo ChangesManager::createDiffInfo(
const QString &originalContent,
const QString &modifiedContent,
@@ -1390,263 +1089,4 @@ ChangesManager::DiffInfo ChangesManager::createDiffInfo(
return diffInfo;
}
bool ChangesManager::findHunkLocation(
const QStringList &fileLines,
const DiffHunk &hunk,
int &actualStartLine,
QString *debugInfo) const
{
LOG_MESSAGE(QString(" Searching for hunk location (expected line: %1)").arg(hunk.oldStartLine));
QString debug;
int expectedIdx = hunk.oldStartLine - 1;
if (expectedIdx >= 0 && expectedIdx < fileLines.size()) {
bool exactMatch = true;
int checkIdx = expectedIdx - hunk.contextBefore.size();
if (checkIdx < 0) {
exactMatch = false;
debug += QString(" Context before out of bounds (need %1 lines before line %2)\n")
.arg(hunk.contextBefore.size()).arg(expectedIdx + 1);
} else {
for (int i = 0; i < hunk.contextBefore.size(); ++i) {
if (fileLines[checkIdx + i] != hunk.contextBefore[i]) {
exactMatch = false;
debug += QString(" Context before mismatch at offset %1: expected '%2', got '%3'\n")
.arg(i).arg(hunk.contextBefore[i]).arg(fileLines[checkIdx + i]);
break;
}
}
}
if (exactMatch) {
for (int i = 0; i < hunk.removedLines.size(); ++i) {
int lineIdx = expectedIdx + i;
if (lineIdx >= fileLines.size() || fileLines[lineIdx] != hunk.removedLines[i]) {
exactMatch = false;
debug += QString(" Removed line mismatch at offset %1: expected '%2', got '%3'\n")
.arg(i)
.arg(hunk.removedLines[i])
.arg(lineIdx < fileLines.size() ? fileLines[lineIdx] : "<EOF>");
break;
}
}
}
if (exactMatch && !hunk.contextAfter.isEmpty()) {
int afterIdx = expectedIdx + hunk.removedLines.size();
for (int i = 0; i < hunk.contextAfter.size(); ++i) {
int lineIdx = afterIdx + i;
if (lineIdx >= fileLines.size() || fileLines[lineIdx] != hunk.contextAfter[i]) {
exactMatch = false;
debug += QString(" Context after mismatch at offset %1: expected '%2', got '%3'\n")
.arg(i)
.arg(hunk.contextAfter[i])
.arg(lineIdx < fileLines.size() ? fileLines[lineIdx] : "<EOF>");
break;
}
}
}
if (exactMatch) {
actualStartLine = expectedIdx;
LOG_MESSAGE(QString(" ✓ Found exact match at expected line %1").arg(hunk.oldStartLine));
if (debugInfo) *debugInfo = "Exact match at expected location";
return true;
} else {
debug += " Exact match at expected location failed, trying fuzzy search...\n";
}
} else {
debug += QString(" Expected location %1 is out of bounds (file has %2 lines)\n")
.arg(hunk.oldStartLine).arg(fileLines.size());
}
LOG_MESSAGE(" Trying fuzzy search within ±20 lines...");
int searchStart = qMax(0, expectedIdx - 20);
int searchEnd = qMin(fileLines.size(), expectedIdx + 20);
int bestMatchLine = -1;
int bestMatchScore = 0;
for (int searchIdx = searchStart; searchIdx < searchEnd; ++searchIdx) {
int matchScore = 0;
int totalChecks = 0;
int checkIdx = searchIdx - hunk.contextBefore.size();
if (checkIdx >= 0) {
for (int i = 0; i < hunk.contextBefore.size(); ++i) {
totalChecks++;
if (fileLines[checkIdx + i] == hunk.contextBefore[i]) {
matchScore++;
}
}
}
for (int i = 0; i < hunk.removedLines.size(); ++i) {
int lineIdx = searchIdx + i;
if (lineIdx < fileLines.size()) {
totalChecks++;
if (fileLines[lineIdx] == hunk.removedLines[i]) {
matchScore++;
}
}
}
int afterIdx = searchIdx + hunk.removedLines.size();
for (int i = 0; i < hunk.contextAfter.size(); ++i) {
int lineIdx = afterIdx + i;
if (lineIdx < fileLines.size()) {
totalChecks++;
if (fileLines[lineIdx] == hunk.contextAfter[i]) {
matchScore++;
}
}
}
if (matchScore > bestMatchScore) {
bestMatchScore = matchScore;
bestMatchLine = searchIdx;
}
}
int totalPossibleScore = hunk.contextBefore.size() + hunk.removedLines.size() + hunk.contextAfter.size();
double matchPercentage = totalPossibleScore > 0 ? (double)bestMatchScore / totalPossibleScore * 100.0 : 0.0;
if (bestMatchLine != -1 && matchPercentage >= 70.0) {
actualStartLine = bestMatchLine;
debug += QString(" ✓ Found fuzzy match at line %1 (score: %2/%3 = %4%%)\n")
.arg(bestMatchLine + 1)
.arg(bestMatchScore)
.arg(totalPossibleScore)
.arg(matchPercentage, 0, 'f', 1);
LOG_MESSAGE(QString(" ✓ Found fuzzy match at line %1 (%2%% confidence)")
.arg(bestMatchLine + 1).arg(matchPercentage, 0, 'f', 1));
if (debugInfo) *debugInfo = debug;
return true;
}
debug += QString(" ✗ No suitable match found (best: %1%% at line %2)\n")
.arg(matchPercentage, 0, 'f', 1)
.arg(bestMatchLine != -1 ? bestMatchLine + 1 : -1);
LOG_MESSAGE(QString(" ✗ Hunk location not found (best match: %1%%)").arg(matchPercentage, 0, 'f', 1));
if (debugInfo) *debugInfo = debug;
return false;
}
bool ChangesManager::applyDiffToContent(
QString &content,
const DiffInfo &diffInfo,
bool reverse,
QString *errorMsg)
{
LOG_MESSAGE(QString("=== Applying %1 to content ===").arg(reverse ? "REVERSE diff" : "diff"));
auto setError = [errorMsg](const QString &msg) {
if (errorMsg) *errorMsg = msg;
};
if (diffInfo.useFallback) {
LOG_MESSAGE(" Using fallback mode (direct content replacement)");
QString searchContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
QString replaceContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
int matchPos = content.indexOf(searchContent);
if (matchPos != -1) {
content = content.left(matchPos)
+ replaceContent
+ content.mid(matchPos + searchContent.length());
setError("Applied using fallback mode (direct replacement)");
LOG_MESSAGE(QString(" ✓ Fallback: Direct replacement successful at position %1").arg(matchPos));
return true;
} else {
setError("Fallback failed: Original content not found in file");
LOG_MESSAGE(" ✗ Fallback: Content not found");
return false;
}
}
if (diffInfo.hunks.isEmpty()) {
LOG_MESSAGE(" No hunks to apply (content unchanged)");
setError("No changes to apply");
return true;
}
QStringList fileLines = content.split('\n');
LOG_MESSAGE(QString(" File has %1 lines, applying %2 hunk(s)")
.arg(fileLines.size()).arg(diffInfo.hunks.size()));
QList<DiffHunk> hunksToApply = diffInfo.hunks;
std::sort(hunksToApply.begin(), hunksToApply.end(),
[](const DiffHunk &a, const DiffHunk &b) {
return a.oldStartLine > b.oldStartLine;
});
LOG_MESSAGE(" Hunks sorted in descending order for application");
int appliedHunks = 0;
int failedHunks = 0;
for (int hunkIdx = 0; hunkIdx < hunksToApply.size(); ++hunkIdx) {
const DiffHunk &hunk = hunksToApply[hunkIdx];
LOG_MESSAGE(QString(" --- Applying hunk %1/%2 ---")
.arg(hunkIdx + 1).arg(hunksToApply.size()));
int actualStartLine = -1;
QString debugInfo;
if (!findHunkLocation(fileLines, hunk, actualStartLine, &debugInfo)) {
LOG_MESSAGE(QString(" ✗ Failed to locate hunk %1:\n%2")
.arg(hunkIdx + 1).arg(debugInfo));
failedHunks++;
continue;
}
LOG_MESSAGE(QString(" Applying hunk at line %1 (remove %2 lines, add %3 lines)")
.arg(actualStartLine + 1)
.arg(hunk.removedLines.size())
.arg(hunk.addedLines.size()));
for (int i = 0; i < hunk.removedLines.size(); ++i) {
if (actualStartLine < fileLines.size()) {
LOG_MESSAGE(QString(" Removing line %1: '%2'")
.arg(actualStartLine + 1)
.arg(fileLines[actualStartLine]));
fileLines.removeAt(actualStartLine);
}
}
for (int i = 0; i < hunk.addedLines.size(); ++i) {
LOG_MESSAGE(QString(" Inserting line %1: '%2'")
.arg(actualStartLine + i + 1)
.arg(hunk.addedLines[i]));
fileLines.insert(actualStartLine + i, hunk.addedLines[i]);
}
appliedHunks++;
LOG_MESSAGE(QString(" ✓ Hunk %1 applied successfully").arg(hunkIdx + 1));
}
if (failedHunks > 0) {
QString msg = QString("Partially applied: %1 of %2 hunks succeeded")
.arg(appliedHunks).arg(hunksToApply.size());
setError(msg);
LOG_MESSAGE(QString(" ⚠ %1").arg(msg));
content = fileLines.join('\n');
return false;
}
content = fileLines.join('\n');
setError(QString("Successfully applied %1 hunk(s)").arg(appliedHunks));
LOG_MESSAGE(QString("=== All %1 hunk(s) applied successfully ===").arg(appliedHunks));
return true;
}
} // namespace QodeAssist::Context

View File

@@ -81,8 +81,7 @@ public:
bool rejectFileEdit(const QString &editId);
bool undoFileEdit(const QString &editId);
FileEdit getFileEdit(const QString &editId) const;
QList<FileEdit> getPendingEdits() const;
bool applyPendingEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
QList<FileEdit> getEditsForRequest(const QString &requestId) const;
@@ -106,13 +105,9 @@ private:
ChangesManager(const ChangesManager &) = delete;
ChangesManager &operator=(const ChangesManager &) = delete;
bool performFileEdit(const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg = nullptr);
bool performFileEditWithDiff(const QString &filePath, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
QString readFileContent(const QString &filePath) const;
DiffInfo createDiffInfo(const QString &originalContent, const QString &modifiedContent, const QString &filePath);
bool applyDiffToContent(QString &content, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
bool findHunkLocation(const QStringList &fileLines, const DiffHunk &hunk, int &actualStartLine, QString *debugInfo = nullptr) const;
// Helper method for fragment-based apply/undo operations
bool performFragmentReplacement(

View File

@@ -6,25 +6,24 @@
#include <QFile>
#include <QFileInfo>
#include <QJsonObject>
#include <QTextStream>
#include "settings/GeneralSettings.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>
#include <texteditor/textdocument.h>
#include "Logger.hpp"
#include "ProjectScannerQtCreator.hpp"
namespace QodeAssist::Context {
ContextManager::ContextManager(QObject *parent)
: QObject(parent)
, m_ignoreManager(new IgnoreManager(this))
: ContextManager(std::make_unique<ProjectScannerQtCreator>(), parent)
{}
ContextManager::ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent)
: QObject(parent)
, m_scanner(std::move(scanner))
{}
ContextManager::~ContextManager() = default;
QString ContextManager::readFile(const QString &filePath) const
{
QFile file(filePath);
@@ -37,7 +36,7 @@ QString ContextManager::readFile(const QString &filePath) const
QTextStream in(&file);
QString content = in.readAll();
file.close();
return content;
}
@@ -45,9 +44,7 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
{
QList<ContentFile> files;
for (const QString &path : filePaths) {
auto project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(path));
if (project && m_ignoreManager->shouldIgnore(path, project)) {
if (m_scanner->shouldIgnore(path)) {
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
continue;
}
@@ -58,27 +55,6 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
return files;
}
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
{
QStringList sourceFiles;
if (!project)
return sourceFiles;
auto projectNode = project->rootProjectNode();
if (!projectNode)
return sourceFiles;
projectNode->forEachNode(
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) {
sourceFiles.append(fileNode->filePath().toUrlishString());
}
},
nullptr);
return sourceFiles;
}
ContentFile ContextManager::createContentFile(const QString &filePath) const
{
ContentFile contentFile;
@@ -100,77 +76,26 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
{
const auto &generalSettings = Settings::generalSettings();
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
Q_UNUSED(documentInfo)
return false;
}
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const
{
auto documents = Core::DocumentModel::openedDocuments();
QList<QPair<QString, QString>> files;
for (const auto *document : std::as_const(documents)) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
if (!excludeFiles.contains(filePath)) {
files.append({filePath, textDocument->plainText()});
}
}
return files;
}
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
QString ContextManager::openedFilesContext(const QStringList &excludeFiles) const
{
QString context = "User files context:\n";
auto documents = Core::DocumentModel::openedDocuments();
for (const auto *document : documents) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
if (excludeFiles.contains(filePath))
continue;
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
context += QString("File: %1\n").arg(filePath);
context += textDocument->plainText();
for (const auto &file : m_scanner->openedTextFiles(excludeFiles)) {
context += QString("File: %1\n").arg(file.filePath);
context += file.content;
context += "\n";
}
return context;
}
IgnoreManager *ContextManager::ignoreManager() const
bool ContextManager::shouldIgnore(const QString &filePath) const
{
return m_ignoreManager;
return m_scanner->shouldIgnore(filePath);
}
} // namespace QodeAssist::Context

View File

@@ -4,18 +4,16 @@
#pragma once
#include <memory>
#include <QObject>
#include <QString>
#include "ContentFile.hpp"
#include "IContextManager.hpp"
#include "IgnoreManager.hpp"
#include "IProjectScanner.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class ContextManager : public QObject, public IContextManager
@@ -24,22 +22,22 @@ class ContextManager : public QObject, public IContextManager
public:
explicit ContextManager(QObject *parent = nullptr);
~ContextManager() override = default;
ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent = nullptr);
~ContextManager() override;
QString readFile(const QString &filePath) const override;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
ContentFile createContentFile(const QString &filePath) const override;
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
IgnoreManager *ignoreManager() const;
QString openedFilesContext(const QStringList &excludeFiles = QStringList{}) const;
bool shouldIgnore(const QString &filePath) const;
private:
IgnoreManager *m_ignoreManager;
std::unique_ptr<IProjectScanner> m_scanner;
};
} // namespace QodeAssist::Context

View File

@@ -4,13 +4,12 @@
#include "DocumentContextReader.hpp"
#include <languageserverprotocol/lsptypes.h>
#include <QFileInfo>
#include <QTextBlock>
#include "CodeCompletionSettings.hpp"
#include "ChangesManager.h"
#include "EnvBlockFormatter.hpp"
const QRegularExpression &getYearRegex()
{
@@ -108,15 +107,6 @@ QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosi
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
}
QString DocumentContextReader::getLanguageAndFileInfo() const
{
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_mimeType);
QString fileExtension = QFileInfo(m_filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, m_mimeType, m_filePath, fileExtension);
}
CopyrightInfo DocumentContextReader::findCopyright()
{
CopyrightInfo result = {-1, -1, false};
@@ -249,12 +239,7 @@ QString DocumentContextReader::getContextBetween(
return context;
}
CopyrightInfo DocumentContextReader::copyrightInfo() const
{
return m_copyrightInfo;
}
PluginLLMCore::ContextData DocumentContextReader::prepareContext(
Templates::ContextData DocumentContextReader::prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
{
QString contextBefore;
@@ -272,7 +257,9 @@ PluginLLMCore::ContextData DocumentContextReader::prepareContext(
}
QString fileContext;
fileContext.append("\n ").append(getLanguageAndFileInfo());
fileContext.append("\n")
.append(EnvBlockFormatter::formatFile({m_filePath, m_mimeType}))
.append("\n");
if (settings.useProjectChangesCache())
fileContext.append("Recent Project Changes Context:\n ")

View File

@@ -7,7 +7,7 @@
#include <texteditor/textdocument.h>
#include <QTextDocument>
#include <pluginllmcore/ContextData.hpp>
#include <sources/common/ContextData.hpp>
#include <settings/CodeCompletionSettings.hpp>
namespace QodeAssist::Context {
@@ -51,14 +51,11 @@ public:
*/
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
QString getLanguageAndFileInfo() const;
CopyrightInfo findCopyright();
QString getContextBetween(
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const;
CopyrightInfo copyrightInfo() const;
PluginLLMCore::ContextData prepareContext(
Templates::ContextData prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
private:

View File

@@ -0,0 +1,66 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "EnvBlockFormatter.hpp"
#include <languageserverprotocol/lsptypes.h>
#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/target.h>
namespace QodeAssist::Context::EnvBlockFormatter {
ProjectEnv currentProject()
{
ProjectEnv env;
auto *project = ProjectExplorer::ProjectManager::startupProject();
if (!project)
return env;
env.name = project->displayName();
env.sourceRoot = project->projectDirectory().toUrlishString();
if (auto *target = project->activeTarget()) {
if (auto *buildConfig = target->activeBuildConfiguration())
env.buildDir = buildConfig->buildDirectory().toUrlishString();
}
return env;
}
QString formatProject(const ProjectEnv &env)
{
if (env.name.isEmpty() && env.sourceRoot.isEmpty())
return QStringLiteral("# No active project in IDE");
QString out = QStringLiteral("# Active project: %1").arg(env.name);
out += QStringLiteral(
"\n# Project source root: %1"
"\n# All new source files, headers, QML and CMake edits MUST be "
"created or modified under this directory. Use absolute paths "
"rooted here, or project-relative paths.")
.arg(env.sourceRoot);
if (!env.buildDir.isEmpty()) {
out += QStringLiteral(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %1")
.arg(env.buildDir);
}
return out;
}
QString formatFile(const FileEnv &env)
{
const QString language
= LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(env.mimeType);
QString out = QStringLiteral("File information:");
if (!language.isEmpty())
out += QStringLiteral("\nLanguage: %1 (MIME: %2)").arg(language, env.mimeType);
else if (!env.mimeType.isEmpty())
out += QStringLiteral("\nMIME type: %1").arg(env.mimeType);
out += QStringLiteral("\nFile path: %1\n").arg(env.filePath);
return out;
}
} // namespace QodeAssist::Context::EnvBlockFormatter

View File

@@ -0,0 +1,29 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QString>
namespace QodeAssist::Context::EnvBlockFormatter {
struct ProjectEnv
{
QString name;
QString sourceRoot;
QString buildDir;
};
struct FileEnv
{
QString filePath;
QString mimeType;
};
ProjectEnv currentProject();
QString formatProject(const ProjectEnv &env);
QString formatFile(const FileEnv &env);
} // namespace QodeAssist::Context::EnvBlockFormatter

View File

@@ -11,10 +11,6 @@
#include "IDocumentReader.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class IContextManager
@@ -24,7 +20,6 @@ public:
virtual QString readFile(const QString &filePath) const = 0;
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
virtual QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const = 0;
virtual ContentFile createContentFile(const QString &filePath) const = 0;
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;

View File

@@ -0,0 +1,28 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QList>
#include <QString>
#include <QStringList>
namespace QodeAssist::Context {
struct OpenedTextFile
{
QString filePath;
QString content;
};
class IProjectScanner
{
public:
virtual ~IProjectScanner() = default;
virtual QList<OpenedTextFile> openedTextFiles(const QStringList &excludeFiles = {}) const = 0;
virtual bool shouldIgnore(const QString &filePath) const = 0;
};
} // namespace QodeAssist::Context

View File

@@ -234,19 +234,6 @@ void IgnoreManager::removeIgnorePatterns(ProjectExplorer::Project *project)
LOG_MESSAGE(QString("Removed ignore patterns for project: %1").arg(project->displayName()));
}
void IgnoreManager::reloadAllPatterns()
{
QList<ProjectExplorer::Project *> projects = m_projectIgnorePatterns.keys();
for (ProjectExplorer::Project *project : projects) {
if (project) {
reloadIgnorePatterns(project);
}
}
m_ignoreCache.clear();
}
QString IgnoreManager::ignoreFilePath(ProjectExplorer::Project *project) const
{
if (!project) {

View File

@@ -27,8 +27,6 @@ public:
void reloadIgnorePatterns(ProjectExplorer::Project *project);
void removeIgnorePatterns(ProjectExplorer::Project *project);
void reloadAllPatterns();
private slots:
void cleanupConnections();

View File

@@ -0,0 +1,53 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ProjectScannerQtCreator.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <texteditor/textdocument.h>
#include <utils/filepath.h>
#include "IgnoreManager.hpp"
namespace QodeAssist::Context {
ProjectScannerQtCreator::ProjectScannerQtCreator()
: m_ignoreManager(std::make_unique<IgnoreManager>())
{}
ProjectScannerQtCreator::~ProjectScannerQtCreator() = default;
QList<OpenedTextFile> ProjectScannerQtCreator::openedTextFiles(
const QStringList &excludeFiles) const
{
QList<OpenedTextFile> files;
const auto documents = Core::DocumentModel::openedDocuments();
for (const auto *document : documents) {
const auto *textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
const QString filePath = textDocument->filePath().toUrlishString();
if (excludeFiles.contains(filePath))
continue;
if (shouldIgnore(filePath))
continue;
files.append({filePath, textDocument->plainText()});
}
return files;
}
bool ProjectScannerQtCreator::shouldIgnore(const QString &filePath) const
{
auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath));
return project && m_ignoreManager->shouldIgnore(filePath, project);
}
} // namespace QodeAssist::Context

View File

@@ -0,0 +1,28 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <memory>
#include "IProjectScanner.hpp"
namespace QodeAssist::Context {
class IgnoreManager;
class ProjectScannerQtCreator : public IProjectScanner
{
public:
ProjectScannerQtCreator();
~ProjectScannerQtCreator() override;
QList<OpenedTextFile> openedTextFiles(const QStringList &excludeFiles = {}) const override;
bool shouldIgnore(const QString &filePath) const override;
private:
std::unique_ptr<IgnoreManager> m_ignoreManager;
};
} // namespace QodeAssist::Context

View File

@@ -35,25 +35,6 @@ bool ProjectUtils::isFileInProject(const QString &filePath)
return false;
}
QString ProjectUtils::findFileInProject(const QString &filename)
{
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
for (auto project : projects) {
if (!project)
continue;
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
for (const auto &projectFile : std::as_const(projectFiles)) {
if (projectFile.fileName() == filename) {
return projectFile.toFSPathString();
}
}
}
return QString();
}
QString ProjectUtils::getProjectRoot()
{
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();

View File

@@ -26,17 +26,6 @@ public:
*/
static bool isFileInProject(const QString &filePath);
/**
* @brief Find a file in open projects by filename
*
* Searches all open projects for a file matching the given filename.
* If multiple files with the same name exist, returns the first match.
*
* @param filename File name to search for (e.g., "main.cpp")
* @return Absolute file path if found, empty string otherwise
*/
static QString findFileInProject(const QString &filename);
/**
* @brief Get the project root directory
*