feat: Add edit file tool (#249)

* feat: Add edit file tool
* feat: Add icons for action buttons
This commit is contained in:
Petr Mironychev
2025-11-03 08:56:52 +01:00
committed by GitHub
parent e7110810f8
commit 9b90aaa06e
39 changed files with 3732 additions and 344 deletions

View File

@ -29,14 +29,17 @@
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <texteditor/texteditor.h>
#include <utils/theme/theme.h>
#include <utils/utilsicons.h>
#include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp"
#include "ToolsSettings.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "context/ChangesManager.h"
#include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp"
#include "llmcore/RulesLoader.hpp"
@ -78,7 +81,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
&ChatRootView::updateInputTokensCount);
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
setRecentFilePath(QString{});
m_currentMessageRequestId.clear();
updateCurrentMessageEditsStats();
});
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
connect(
@ -138,6 +145,46 @@ ChatRootView::ChatRootView(QQuickItem *parent)
m_lastErrorMessage = error;
emit lastErrorMessageChanged();
});
connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
if (!m_currentMessageRequestId.isEmpty()) {
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId));
}
m_currentMessageRequestId = requestId;
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
updateCurrentMessageEditsStats();
});
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditAdded,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditApplied,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditRejected,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditUndone,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditArchived,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
updateInputTokensCount();
refreshRules();
@ -152,7 +199,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool();
connect(
&Settings::generalSettings().useTools,
&Settings::toolsSettings().useTools,
&Utils::BaseAspect::changed,
this,
&ChatRootView::toolsSupportEnabledChanged);
@ -258,7 +305,10 @@ void ChatRootView::loadHistory(const QString &filePath)
} else {
setRecentFilePath(filePath);
}
m_currentMessageRequestId.clear();
updateInputTokensCount();
updateCurrentMessageEditsStats();
}
void ChatRootView::showSaveDialog()
@ -731,7 +781,310 @@ void ChatRootView::setIsAgentMode(bool newIsAgentMode)
bool ChatRootView::toolsSupportEnabled() const
{
return Settings::generalSettings().useTools();
return Settings::toolsSettings().useTools();
}
void ChatRootView::applyFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
m_lastInfoMessage = QString("File edit applied successfully");
emit lastInfoMessageChanged();
updateFileEditStatus(editId, "applied");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to apply file edit")
: QString("Failed to apply file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged();
}
}
void ChatRootView::rejectFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
m_lastInfoMessage = QString("File edit rejected");
emit lastInfoMessageChanged();
updateFileEditStatus(editId, "rejected");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to reject file edit")
: QString("Failed to reject file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged();
}
}
void ChatRootView::undoFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
m_lastInfoMessage = QString("File edit undone successfully");
emit lastInfoMessageChanged();
updateFileEditStatus(editId, "rejected");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to undo file edit")
: QString("Failed to undo file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged();
}
}
void ChatRootView::openFileEditInEditor(const QString &editId)
{
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (edit.editId.isEmpty()) {
m_lastErrorMessage = QString("File edit not found: %1").arg(editId);
emit lastErrorMessageChanged();
return;
}
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
if (!editor) {
m_lastErrorMessage = QString("Failed to open file in editor: %1").arg(edit.filePath);
emit lastErrorMessageChanged();
return;
}
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
if (textEditor && textEditor->editorWidget()) {
QTextDocument *doc = textEditor->editorWidget()->document();
if (doc) {
QString currentContent = doc->toPlainText();
int position = -1;
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
position = currentContent.indexOf(edit.newContent);
}
else if (!edit.oldContent.isEmpty()) {
position = currentContent.indexOf(edit.oldContent);
}
if (position >= 0) {
QTextCursor cursor(doc);
cursor.setPosition(position);
textEditor->editorWidget()->setTextCursor(cursor);
textEditor->editorWidget()->centerCursor();
}
}
}
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
}
void ChatRootView::updateFileEditStatus(const QString &editId, const QString &status)
{
auto messages = m_chatModel->getChatHistory();
for (int i = 0; i < messages.size(); ++i) {
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
QString content = messages[i].content;
const QString marker = "QODEASSIST_FILE_EDIT:";
int markerPos = content.indexOf(marker);
QString jsonStr = content;
if (markerPos >= 0) {
jsonStr = content.mid(markerPos + marker.length());
}
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject obj = doc.object();
obj["status"] = status;
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (!edit.statusMessage.isEmpty()) {
obj["status_message"] = edit.statusMessage;
}
QString updatedContent = marker + QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
m_chatModel->updateMessageContent(editId, updatedContent);
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
}
break;
}
}
updateCurrentMessageEditsStats();
}
void ChatRootView::applyAllFileEditsForCurrentMessage()
{
if (m_currentMessageRequestId.isEmpty()) {
m_lastErrorMessage = QString("No active message with file edits");
emit lastErrorMessageChanged();
return;
}
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentMessageRequestId));
QString errorMsg;
bool success = Context::ChangesManager::instance()
.reapplyAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
if (success) {
m_lastInfoMessage = QString("All file edits applied successfully");
emit lastInfoMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied");
}
}
} else {
m_lastErrorMessage = errorMsg.isEmpty()
? QString("Failed to apply some file edits")
: QString("Failed to apply some file edits:\n%1").arg(errorMsg);
emit lastErrorMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied");
}
}
}
updateCurrentMessageEditsStats();
}
void ChatRootView::undoAllFileEditsForCurrentMessage()
{
if (m_currentMessageRequestId.isEmpty()) {
m_lastErrorMessage = QString("No active message with file edits");
emit lastErrorMessageChanged();
return;
}
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentMessageRequestId));
QString errorMsg;
bool success = Context::ChangesManager::instance()
.undoAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
if (success) {
m_lastInfoMessage = QString("All file edits undone successfully");
emit lastInfoMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected");
}
}
} else {
m_lastErrorMessage = errorMsg.isEmpty()
? QString("Failed to undo some file edits")
: QString("Failed to undo some file edits:\n%1").arg(errorMsg);
emit lastErrorMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected");
}
}
}
updateCurrentMessageEditsStats();
}
void ChatRootView::updateCurrentMessageEditsStats()
{
if (m_currentMessageRequestId.isEmpty()) {
if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0 ||
m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) {
m_currentMessageTotalEdits = 0;
m_currentMessageAppliedEdits = 0;
m_currentMessagePendingEdits = 0;
m_currentMessageRejectedEdits = 0;
emit currentMessageEditsStatsChanged();
}
return;
}
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
int total = edits.size();
int applied = 0;
int pending = 0;
int rejected = 0;
for (const auto &edit : edits) {
switch (edit.status) {
case Context::ChangesManager::Applied:
applied++;
break;
case Context::ChangesManager::Pending:
pending++;
break;
case Context::ChangesManager::Rejected:
rejected++;
break;
case Context::ChangesManager::Archived:
total--;
break;
}
}
bool changed = false;
if (m_currentMessageTotalEdits != total) {
m_currentMessageTotalEdits = total;
changed = true;
}
if (m_currentMessageAppliedEdits != applied) {
m_currentMessageAppliedEdits = applied;
changed = true;
}
if (m_currentMessagePendingEdits != pending) {
m_currentMessagePendingEdits = pending;
changed = true;
}
if (m_currentMessageRejectedEdits != rejected) {
m_currentMessageRejectedEdits = rejected;
changed = true;
}
if (changed) {
LOG_MESSAGE(QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
.arg(total).arg(applied).arg(pending).arg(rejected));
emit currentMessageEditsStatsChanged();
}
}
int ChatRootView::currentMessageTotalEdits() const
{
return m_currentMessageTotalEdits;
}
int ChatRootView::currentMessageAppliedEdits() const
{
return m_currentMessageAppliedEdits;
}
int ChatRootView::currentMessagePendingEdits() const
{
return m_currentMessagePendingEdits;
}
int ChatRootView::currentMessageRejectedEdits() const
{
return m_currentMessageRejectedEdits;
}
QString ChatRootView::lastInfoMessage() const
{
return m_lastInfoMessage;
}
} // namespace QodeAssist::Chat