feat: add support acp in common chat (#369)

This commit is contained in:
Petr Mironychev
2026-07-21 13:10:00 +02:00
committed by GitHub
parent af85efb554
commit 442263a6a7
158 changed files with 14270 additions and 4118 deletions

View File

@@ -1,6 +1,6 @@
add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp
ChangesManager.h ChangesManager.cpp
FileEditManager.hpp FileEditManager.cpp
ContextManager.hpp ContextManager.cpp
ContentFile.hpp
DocumentReaderQtCreator.hpp
@@ -10,7 +10,6 @@ add_library(Context STATIC
IContextManager.hpp
IgnoreManager.hpp IgnoreManager.cpp
ProjectUtils.hpp ProjectUtils.cpp
RulesLoader.hpp RulesLoader.cpp
)
target_link_libraries(Context

View File

@@ -12,6 +12,7 @@ struct ContentFile
{
QString filename;
QString content;
QString path;
};
} // namespace QodeAssist::Context

View File

@@ -41,9 +41,9 @@ QString ContextManager::readFile(const QString &filePath) const
return content;
}
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
QStringList ContextManager::allowedPaths(const QStringList &filePaths) const
{
QList<ContentFile> files;
QStringList allowed;
for (const QString &path : filePaths) {
auto project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(path));
@@ -52,9 +52,16 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
continue;
}
ContentFile contentFile = createContentFile(path);
files.append(contentFile);
allowed.append(path);
}
return allowed;
}
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
{
QList<ContentFile> files;
for (const QString &path : allowedPaths(filePaths))
files.append(createContentFile(path));
return files;
}
@@ -85,6 +92,7 @@ ContentFile ContextManager::createContentFile(const QString &filePath) const
QFileInfo fileInfo(filePath);
contentFile.filename = fileInfo.fileName();
contentFile.content = readFile(filePath);
contentFile.path = fileInfo.absoluteFilePath();
return contentFile;
}

View File

@@ -27,6 +27,7 @@ public:
~ContextManager() override = default;
QString readFile(const QString &filePath) const override;
QStringList allowedPaths(const QStringList &filePaths) const override;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
ContentFile createContentFile(const QString &filePath) const override;

View File

@@ -10,8 +10,6 @@
#include "CodeCompletionSettings.hpp"
#include "ChangesManager.h"
const QRegularExpression &getYearRegex()
{
static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b");
@@ -274,10 +272,6 @@ LLMCore::ContextData DocumentContextReader::prepareContext(
QString fileContext;
fileContext.append("\n ").append(getLanguageAndFileInfo());
if (settings.useProjectChangesCache())
fileContext.append("Recent Project Changes Context:\n ")
.append(ChangesManager::instance().getRecentChangesContext(m_textDocument));
return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext};
}

View File

@@ -2,11 +2,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChangesManager.h"
#include "CodeCompletionSettings.hpp"
#include "FileEditManager.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <texteditor/textdocument.h>
#include <logger/Logger.hpp>
#include <algorithm>
#include <QFile>
@@ -15,61 +15,20 @@
namespace QodeAssist::Context {
ChangesManager &ChangesManager::instance()
FileEditManager &FileEditManager::instance()
{
static ChangesManager instance;
static FileEditManager instance;
return instance;
}
ChangesManager::ChangesManager()
FileEditManager::FileEditManager()
: QObject(nullptr)
, m_undoStack(new QUndoStack(this))
{}
ChangesManager::~ChangesManager() {}
FileEditManager::~FileEditManager() {}
void ChangesManager::addChange(
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded)
{
auto &documentQueue = m_documentChanges[document];
QTextBlock block = document->document()->findBlock(position);
int lineNumber = block.blockNumber();
QString lineContent = block.text();
QString fileName = document->filePath().fileName();
ChangeInfo change{fileName, lineNumber, lineContent};
auto it
= std::find_if(documentQueue.begin(), documentQueue.end(), [lineNumber](const ChangeInfo &c) {
return c.lineNumber == lineNumber;
});
if (it != documentQueue.end()) {
it->lineContent = lineContent;
} else {
documentQueue.enqueue(change);
if (documentQueue.size() > Settings::codeCompletionSettings().maxChangesCacheSize()) {
documentQueue.dequeue();
}
}
}
QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const
{
QString context;
for (auto it = m_documentChanges.constBegin(); it != m_documentChanges.constEnd(); ++it) {
if (it.key() != currentDocument) {
for (const auto &change : it.value()) {
context += change.lineContent + "\n";
}
}
}
return context;
}
void ChangesManager::addFileEdit(
void FileEditManager::addFileEdit(
const QString &editId,
const QString &filePath,
const QString &oldContent,
@@ -139,7 +98,44 @@ void ChangesManager::addFileEdit(
}
}
bool ChangesManager::applyFileEdit(const QString &editId)
void FileEditManager::registerAppliedFileEdit(
const QString &editId,
const QString &filePath,
const QString &oldContent,
const QString &newContent,
const QString &requestId)
{
QMutexLocker locker(&m_mutex);
if (m_fileEdits.contains(editId)) {
LOG_MESSAGE(QString("File edit already exists, skipping: %1").arg(editId));
return;
}
FileEdit edit;
edit.editId = editId;
edit.filePath = filePath;
edit.oldContent = oldContent;
edit.newContent = newContent;
edit.timestamp = QDateTime::currentDateTime();
edit.wasAutoApplied = true;
edit.isFromHistory = false;
edit.status = Applied;
edit.statusMessage = "Applied by agent";
m_fileEdits.insert(editId, edit);
if (!requestId.isEmpty())
m_requestEdits[requestId].editIds.append(editId);
locker.unlock();
emit fileEditAdded(editId);
LOG_MESSAGE(QString("Agent file edit registered as applied: %1 for file %2")
.arg(editId, filePath));
}
bool FileEditManager::applyFileEdit(const QString &editId)
{
QMutexLocker locker(&m_mutex);
@@ -193,7 +189,7 @@ bool ChangesManager::applyFileEdit(const QString &editId)
return false;
}
bool ChangesManager::rejectFileEdit(const QString &editId)
bool FileEditManager::rejectFileEdit(const QString &editId)
{
QMutexLocker locker(&m_mutex);
@@ -220,7 +216,7 @@ bool ChangesManager::rejectFileEdit(const QString &editId)
return true;
}
bool ChangesManager::undoFileEdit(const QString &editId)
bool FileEditManager::undoFileEdit(const QString &editId)
{
QMutexLocker locker(&m_mutex);
@@ -276,13 +272,13 @@ bool ChangesManager::undoFileEdit(const QString &editId)
return false;
}
ChangesManager::FileEdit ChangesManager::getFileEdit(const QString &editId) const
FileEditManager::FileEdit FileEditManager::getFileEdit(const QString &editId) const
{
QMutexLocker locker(&m_mutex);
return m_fileEdits.value(editId);
}
QList<ChangesManager::FileEdit> ChangesManager::getPendingEdits() const
QList<FileEditManager::FileEdit> FileEditManager::getPendingEdits() const
{
QMutexLocker locker(&m_mutex);
@@ -295,7 +291,7 @@ QList<ChangesManager::FileEdit> ChangesManager::getPendingEdits() const
return pendingEdits;
}
bool ChangesManager::performFileEdit(
bool FileEditManager::performFileEdit(
const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg)
{
auto setError = [errorMsg](const QString &msg) {
@@ -451,7 +447,7 @@ bool ChangesManager::performFileEdit(
return true;
}
int ChangesManager::levenshteinDistance(const QString &s1, const QString &s2) const
int FileEditManager::levenshteinDistance(const QString &s1, const QString &s2) const
{
const int len1 = s1.length();
const int len2 = s2.length();
@@ -484,7 +480,7 @@ int ChangesManager::levenshteinDistance(const QString &s1, const QString &s2) co
return d[len1][len2];
}
QString ChangesManager::findBestMatchLineBased(
QString FileEditManager::findBestMatchLineBased(
const QString &fileContent,
const QString &searchContent,
double threshold,
@@ -550,7 +546,7 @@ QString ChangesManager::findBestMatchLineBased(
return bestMatch;
}
QString ChangesManager::findBestMatch(const QString &fileContent, const QString &searchContent, double threshold, double *outSimilarity) const
QString FileEditManager::findBestMatch(const QString &fileContent, const QString &searchContent, double threshold, double *outSimilarity) const
{
if (searchContent.isEmpty() || fileContent.isEmpty()) {
if (outSimilarity) *outSimilarity = 0.0;
@@ -631,7 +627,7 @@ QString ChangesManager::findBestMatch(const QString &fileContent, const QString
return bestMatch;
}
QString ChangesManager::findBestMatchWithNormalization(
QString FileEditManager::findBestMatchWithNormalization(
const QString &fileContent,
const QString &searchContent,
double *outSimilarity,
@@ -677,7 +673,7 @@ QString ChangesManager::findBestMatchWithNormalization(
return QString();
}
bool ChangesManager::performFragmentReplacement(
bool FileEditManager::performFragmentReplacement(
const QString &filePath,
const QString &searchContent,
const QString &replaceContent,
@@ -821,7 +817,7 @@ bool ChangesManager::performFragmentReplacement(
return true;
}
bool ChangesManager::applyPendingEditsForRequest(const QString &requestId, QString *errorMsg)
bool FileEditManager::applyPendingEditsForRequest(const QString &requestId, QString *errorMsg)
{
QMutexLocker locker(&m_mutex);
@@ -857,7 +853,7 @@ bool ChangesManager::applyPendingEditsForRequest(const QString &requestId, QStri
return true;
}
QList<ChangesManager::FileEdit> ChangesManager::getEditsForRequest(const QString &requestId) const
QList<FileEditManager::FileEdit> FileEditManager::getEditsForRequest(const QString &requestId) const
{
QMutexLocker locker(&m_mutex);
@@ -877,7 +873,7 @@ QList<ChangesManager::FileEdit> ChangesManager::getEditsForRequest(const QString
return edits;
}
bool ChangesManager::undoAllEditsForRequest(const QString &requestId, QString *errorMsg)
bool FileEditManager::undoAllEditsForRequest(const QString &requestId, QString *errorMsg)
{
QMutexLocker locker(&m_mutex);
@@ -962,7 +958,7 @@ bool ChangesManager::undoAllEditsForRequest(const QString &requestId, QString *e
return true;
}
bool ChangesManager::reapplyAllEditsForRequest(const QString &requestId, QString *errorMsg)
bool FileEditManager::reapplyAllEditsForRequest(const QString &requestId, QString *errorMsg)
{
QMutexLocker locker(&m_mutex);
@@ -1046,7 +1042,7 @@ bool ChangesManager::reapplyAllEditsForRequest(const QString &requestId, QString
return true;
}
void ChangesManager::archiveAllNonArchivedEdits()
void FileEditManager::archiveAllNonArchivedEdits()
{
QMutexLocker locker(&m_mutex);
@@ -1080,7 +1076,7 @@ void ChangesManager::archiveAllNonArchivedEdits()
}
}
QString ChangesManager::readFileContent(const QString &filePath) const
QString FileEditManager::readFileContent(const QString &filePath) const
{
LOG_MESSAGE(QString("Reading current file content: %1").arg(filePath));
@@ -1112,7 +1108,7 @@ QString ChangesManager::readFileContent(const QString &filePath) const
return content;
}
bool ChangesManager::performFileEditWithDiff(
bool FileEditManager::performFileEditWithDiff(
const QString &filePath,
const DiffInfo &diffInfo,
bool reverse,
@@ -1244,7 +1240,7 @@ bool ChangesManager::performFileEditWithDiff(
return true;
}
ChangesManager::DiffInfo ChangesManager::createDiffInfo(
FileEditManager::DiffInfo FileEditManager::createDiffInfo(
const QString &originalContent,
const QString &modifiedContent,
const QString &filePath)
@@ -1390,7 +1386,7 @@ ChangesManager::DiffInfo ChangesManager::createDiffInfo(
return diffInfo;
}
bool ChangesManager::findHunkLocation(
bool FileEditManager::findHunkLocation(
const QStringList &fileLines,
const DiffHunk &hunk,
int &actualStartLine,
@@ -1537,7 +1533,7 @@ bool ChangesManager::findHunkLocation(
return false;
}
bool ChangesManager::applyDiffToContent(
bool FileEditManager::applyDiffToContent(
QString &content,
const DiffInfo &diffInfo,
bool reverse,

View File

@@ -4,28 +4,22 @@
#pragma once
#include <texteditor/textdocument.h>
#include <QDateTime>
#include <QHash>
#include <QList>
#include <QMutex>
#include <QQueue>
#include <QTimer>
#include <QObject>
#include <QString>
#include <QStringList>
#include <QUndoStack>
namespace QodeAssist::Context {
class ChangesManager : public QObject
class FileEditManager : public QObject
{
Q_OBJECT
public:
struct ChangeInfo
{
QString fileName;
int lineNumber;
QString lineContent;
};
enum FileEditStatus { Pending, Applied, Rejected, Archived };
struct DiffHunk
@@ -63,11 +57,7 @@ public:
QString statusMessage;
};
static ChangesManager &instance();
void addChange(
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
static FileEditManager &instance();
void addFileEdit(
const QString &editId,
@@ -77,20 +67,26 @@ public:
bool autoApply = true,
bool isFromHistory = false,
const QString &requestId = QString());
void registerAppliedFileEdit(
const QString &editId,
const QString &filePath,
const QString &oldContent,
const QString &newContent,
const QString &requestId = QString());
bool applyFileEdit(const QString &editId);
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;
bool undoAllEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
bool reapplyAllEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
void archiveAllNonArchivedEdits();
signals:
@@ -101,19 +97,19 @@ signals:
void fileEditArchived(const QString &editId);
private:
ChangesManager();
~ChangesManager();
ChangesManager(const ChangesManager &) = delete;
ChangesManager &operator=(const ChangesManager &) = delete;
FileEditManager();
~FileEditManager();
FileEditManager(const FileEditManager &) = delete;
FileEditManager &operator=(const FileEditManager &) = 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(
const QString &filePath,
@@ -122,7 +118,7 @@ private:
bool isAppendOperation,
QString *errorMsg = nullptr,
bool isUndo = false);
int levenshteinDistance(const QString &s1, const QString &s2) const;
QString findBestMatch(const QString &fileContent, const QString &searchContent, double threshold = 0.82, double *outSimilarity = nullptr) const;
QString findBestMatchLineBased(const QString &fileContent, const QString &searchContent, double threshold = 0.82, double *outSimilarity = nullptr) const;
@@ -134,7 +130,6 @@ private:
bool autoApplyPending = false;
};
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
QHash<QString, FileEdit> m_fileEdits;
QHash<QString, RequestEdits> m_requestEdits; // requestId → ordered edits
QUndoStack *m_undoStack;

View File

@@ -23,6 +23,7 @@ public:
virtual ~IContextManager() = default;
virtual QString readFile(const QString &filePath) const = 0;
virtual QStringList allowedPaths(const QStringList &filePaths) 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;

View File

@@ -1,166 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "context/RulesLoader.hpp"
#include <QDir>
#include <QFile>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
namespace QodeAssist::Context {
QString RulesLoader::loadRules(const QString &projectPath, RulesContext context)
{
if (projectPath.isEmpty()) {
return QString();
}
QString combined;
QString basePath = projectPath + "/.qodeassist/rules";
switch (context) {
case RulesContext::Completions:
combined += loadAllMarkdownFiles(basePath + "/completions");
break;
case RulesContext::Chat:
combined += loadAllMarkdownFiles(basePath + "/common");
combined += loadAllMarkdownFiles(basePath + "/chat");
break;
case RulesContext::QuickRefactor:
combined += loadAllMarkdownFiles(basePath + "/common");
combined += loadAllMarkdownFiles(basePath + "/quickrefactor");
break;
}
return combined;
}
QString RulesLoader::loadRulesForProject(ProjectExplorer::Project *project, RulesContext context)
{
if (!project) {
return QString();
}
QString projectPath = getProjectPath(project);
return loadRules(projectPath, context);
}
ProjectExplorer::Project *RulesLoader::getActiveProject()
{
auto currentEditor = Core::EditorManager::currentEditor();
if (currentEditor && currentEditor->document()) {
Utils::FilePath filePath = currentEditor->document()->filePath();
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project) {
return project;
}
}
return ProjectExplorer::ProjectManager::startupProject();
}
QString RulesLoader::loadAllMarkdownFiles(const QString &dirPath)
{
QString combined;
QDir dir(dirPath);
if (!dir.exists()) {
return QString();
}
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
for (const QString &fileName : mdFiles) {
QFile file(dir.filePath(fileName));
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
combined += file.readAll();
combined += "\n\n";
}
}
return combined;
}
QString RulesLoader::getProjectPath(ProjectExplorer::Project *project)
{
if (!project) {
return QString();
}
return project->projectDirectory().toUrlishString();
}
QVector<RuleFileInfo> RulesLoader::getRuleFiles(const QString &projectPath, RulesContext context)
{
if (projectPath.isEmpty()) {
return QVector<RuleFileInfo>();
}
QVector<RuleFileInfo> result;
QString basePath = projectPath + "/.qodeassist/rules";
// Always include common rules
result.append(collectMarkdownFiles(basePath + "/common", "common"));
// Add context-specific rules
switch (context) {
case RulesContext::Completions:
result.append(collectMarkdownFiles(basePath + "/completions", "completions"));
break;
case RulesContext::Chat:
result.append(collectMarkdownFiles(basePath + "/chat", "chat"));
break;
case RulesContext::QuickRefactor:
result.append(collectMarkdownFiles(basePath + "/quickrefactor", "quickrefactor"));
break;
}
return result;
}
QVector<RuleFileInfo> RulesLoader::getRuleFilesForProject(
ProjectExplorer::Project *project, RulesContext context)
{
if (!project) {
return QVector<RuleFileInfo>();
}
QString projectPath = getProjectPath(project);
return getRuleFiles(projectPath, context);
}
QString RulesLoader::loadRuleFileContent(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return QString();
}
return file.readAll();
}
QVector<RuleFileInfo> RulesLoader::collectMarkdownFiles(
const QString &dirPath, const QString &category)
{
QVector<RuleFileInfo> result;
QDir dir(dirPath);
if (!dir.exists()) {
return result;
}
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
for (const QString &fileName : mdFiles) {
QString fullPath = dir.filePath(fileName);
result.append({fullPath, fileName, category});
}
return result;
}
} // namespace QodeAssist::Context

View File

@@ -1,42 +0,0 @@
// Copyright (C) 2025-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 ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
enum class RulesContext { Completions, Chat, QuickRefactor };
struct RuleFileInfo
{
QString filePath;
QString fileName;
QString category; // "common", "chat", "completions", "quickrefactor"
};
class RulesLoader
{
public:
static QString loadRules(const QString &projectPath, RulesContext context);
static QString loadRulesForProject(ProjectExplorer::Project *project, RulesContext context);
static ProjectExplorer::Project *getActiveProject();
// New methods for getting rule files info
static QVector<RuleFileInfo> getRuleFiles(const QString &projectPath, RulesContext context);
static QVector<RuleFileInfo> getRuleFilesForProject(ProjectExplorer::Project *project, RulesContext context);
static QString loadRuleFileContent(const QString &filePath);
private:
static QString loadAllMarkdownFiles(const QString &dirPath);
static QVector<RuleFileInfo> collectMarkdownFiles(const QString &dirPath, const QString &category);
static QString getProjectPath(ProjectExplorer::Project *project);
};
} // namespace QodeAssist::Context