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

@@ -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