diff --git a/CMakeLists.txt b/CMakeLists.txt index 5de9ddd..dd7bfe4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,7 +122,7 @@ add_qtc_plugin(QodeAssist tools/ToolsManager.hpp tools/ToolsManager.cpp tools/SearchInProjectTool.hpp tools/SearchInProjectTool.cpp tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp - tools/EditFileTool.hpp tools/EditFileTool.cpp + tools/FindSymbolTool.hpp tools/FindSymbolTool.cpp tools/FindFileTool.hpp tools/FindFileTool.cpp tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp diff --git a/ChatView/CMakeLists.txt b/ChatView/CMakeLists.txt index 63107a7..2135402 100644 --- a/ChatView/CMakeLists.txt +++ b/ChatView/CMakeLists.txt @@ -18,7 +18,6 @@ qt_add_qml_module(QodeAssistChatView qml/parts/AttachedFilesPlace.qml qml/parts/ErrorToast.qml qml/ToolStatusItem.qml - qml/FileEditChangesItem.qml qml/parts/RulesViewer.qml RESOURCES @@ -48,7 +47,7 @@ qt_add_qml_module(QodeAssistChatView ChatSerializer.hpp ChatSerializer.cpp ChatView.hpp ChatView.cpp ChatData.hpp - FileEditItem.hpp FileEditItem.cpp + ) target_link_libraries(QodeAssistChatView diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index 6bec293..95eafee 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -92,8 +92,6 @@ void ClientInterface::sendMessage( "**Workflow patterns:**\n" "- Code structure: find_cpp_symbol → read_files_by_path\n" "- Find usages: find_cpp_symbol → search_in_project\n" - "- Fix errors: get_issues_list → find_cpp_symbol → read_files_by_path → edit_file\n" - "- Refactoring: find_cpp_symbol → read_files → search_in_project → edit_file\n\n" "**Best practices:**\n" "- Prefer find_cpp_symbol over search_in_project for code symbols\n" "- Read once, edit comprehensively (atomic edits)\n" diff --git a/ChatView/FileEditItem.cpp b/ChatView/FileEditItem.cpp deleted file mode 100644 index 647534f..0000000 --- a/ChatView/FileEditItem.cpp +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright (C) 2025 Petr Mironychev - * - * This file is part of QodeAssist. - * - * QodeAssist is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QodeAssist is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QodeAssist. If not, see . - */ - -#include "FileEditItem.hpp" - -#include "Logger.hpp" -#include "settings/GeneralSettings.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Chat { - -QMutex FileEditItem::s_fileLockMutex; -QSet FileEditItem::s_lockedFiles; - -FileEditItem::FileEditItem(QQuickItem *parent) - : QQuickItem(parent) -{} - -void FileEditItem::parseFromContent(const QString &content) -{ - static const QLatin1String marker(EDIT_MARKER); - int markerPos = content.indexOf(marker); - - if (markerPos == -1) { - return; - } - - int jsonStart = markerPos + marker.size(); - QString jsonStr = content.mid(jsonStart); - - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError); - - if (parseError.error != QJsonParseError::NoError) { - return; - } - - if (!doc.isObject()) { - return; - } - - QJsonObject editData = doc.object(); - - m_editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch()); - m_mode = editData["mode"].toString(); - m_filePath = editData["filepath"].toString(); - m_newText = editData["new_text"].toString(); - m_searchText = editData["search_text"].toString(); - m_lineBefore = editData["line_before"].toString(); - m_lineAfter = editData["line_after"].toString(); - - if (m_mode.isEmpty()) { - m_mode = m_searchText.isEmpty() ? "insert_after" : "replace"; - } - - m_addedLines = m_newText.split('\n').size(); - m_removedLines = m_searchText.isEmpty() ? 0 : m_searchText.split('\n').size(); - - emit editIdChanged(); - emit modeChanged(); - emit filePathChanged(); - emit searchTextChanged(); - emit newTextChanged(); - emit lineBeforeChanged(); - emit lineAfterChanged(); - emit addedLinesChanged(); - emit removedLinesChanged(); - - bool autoApplyEnabled = Settings::generalSettings().autoApplyFileEdits.value(); - if (autoApplyEnabled) { - applyEditInternal(true); - } -} - -void FileEditItem::applyEdit() -{ - applyEditInternal(false, 0); -} - -void FileEditItem::applyEditInternal(bool isAutomatic, int retryCount) -{ - if (isAutomatic) { - if (m_status != EditStatus::Pending) { - return; - } - } else { - if (m_status != EditStatus::Pending && m_status != EditStatus::Reverted - && m_status != EditStatus::Rejected) { - return; - } - } - - if (!acquireFileLock(m_filePath)) { - if (retryCount >= MAX_RETRY_COUNT) { - rejectWithError(QString("File %1 is locked, exceeded retry limit").arg(m_filePath)); - return; - } - - const int retryDelay = isAutomatic ? AUTO_APPLY_RETRY_DELAY_MS : RETRY_DELAY_MS; - QTimer::singleShot(retryDelay, this, [this, isAutomatic, retryCount]() { - applyEditInternal(isAutomatic, retryCount + 1); - }); - return; - } - - performApply(); - releaseFileLock(m_filePath); -} - -void FileEditItem::revertEdit() -{ - if (m_status != EditStatus::Applied) { - return; - } - - if (!acquireFileLock(m_filePath)) { - QTimer::singleShot(RETRY_DELAY_MS, this, &FileEditItem::revertEdit); - return; - } - - performRevert(); - releaseFileLock(m_filePath); -} - -void FileEditItem::performApply() -{ - QString currentContent = readFile(m_filePath); - m_originalContent = currentContent; - - QString editedContent; - - if (m_mode == "insert_after") { - if (m_lineBefore.isEmpty()) { - editedContent = m_newText + currentContent; - } else { - QList positions; - int pos = 0; - while ((pos = currentContent.indexOf(m_lineBefore, pos)) != -1) { - positions.append(pos); - pos += m_lineBefore.length(); - } - - if (positions.isEmpty()) { - rejectWithError("Failed to apply edit: line_before not found"); - return; - } - - int matchedPosition = -1; - - if (!m_lineAfter.isEmpty()) { - for (int beforePos : positions) { - int afterPos = beforePos + m_lineBefore.length(); - if (afterPos + m_lineAfter.length() <= currentContent.length()) { - QString actualAfter = currentContent.mid(afterPos, m_lineAfter.length()); - if (actualAfter == m_lineAfter) { - matchedPosition = afterPos; - break; - } - } - } - - if (matchedPosition == -1) { - rejectWithError("Failed to apply edit: line_before found but line_after context doesn't match"); - return; - } - } else { - matchedPosition = positions.first() + m_lineBefore.length(); - } - - editedContent = currentContent; - editedContent.insert(matchedPosition, m_newText); - } - - } else if (m_mode == "replace") { - if (m_searchText.isEmpty()) { - rejectWithError("REPLACE mode requires search_text to be specified"); - return; - } - - bool hasLineBefore = !m_lineBefore.isEmpty(); - bool hasLineAfter = !m_lineAfter.isEmpty(); - - QList searchPositions; - int pos = 0; - while ((pos = currentContent.indexOf(m_searchText, pos)) != -1) { - searchPositions.append(pos); - pos += m_searchText.length(); - } - - if (searchPositions.isEmpty()) { - rejectWithError("Failed to apply edit: search_text not found. File may have been modified."); - return; - } - - int matchedPosition = -1; - const int MAX_CONTEXT_DISTANCE = 500; - - for (int searchPos : searchPositions) { - bool beforeMatches = true; - bool afterMatches = true; - - if (hasLineBefore) { - int searchStart = qMax(0, searchPos - MAX_CONTEXT_DISTANCE); - int beforePos = currentContent.lastIndexOf(m_lineBefore, searchPos - 1); - - if (beforePos >= searchStart && beforePos < searchPos) { - beforeMatches = true; - } else { - beforeMatches = false; - } - } - - if (hasLineAfter) { - int afterPos = searchPos + m_searchText.length(); - int searchEnd = qMin(currentContent.length(), afterPos + MAX_CONTEXT_DISTANCE); - int foundAfterPos = currentContent.indexOf(m_lineAfter, afterPos); - - if (foundAfterPos >= afterPos && foundAfterPos < searchEnd) { - afterMatches = true; - } else { - afterMatches = false; - } - } - - bool isMatch = false; - - if (hasLineBefore && hasLineAfter) { - isMatch = beforeMatches && afterMatches; - } else if (hasLineBefore && !hasLineAfter) { - isMatch = beforeMatches; - } else if (!hasLineBefore && hasLineAfter) { - isMatch = afterMatches; - } else { - isMatch = true; - } - - if (isMatch) { - matchedPosition = searchPos; - break; - } - } - - if (matchedPosition == -1) { - rejectWithError("Failed to apply edit: search_text found but context verification failed."); - return; - } - - editedContent = currentContent; - editedContent.replace(matchedPosition, m_searchText.length(), m_newText); - - } else { - rejectWithError(QString("Unknown edit mode: %1").arg(m_mode)); - return; - } - - if (!writeFile(m_filePath, editedContent)) { - rejectWithError(QString("Failed to write file: %1").arg(m_filePath)); - return; - } - - finishWithSuccess(EditStatus::Applied, QString("Successfully applied edit to: %1").arg(m_filePath)); -} - -void FileEditItem::performRevert() -{ - if (!writeFile(m_filePath, m_originalContent)) { - rejectWithError(QString("Failed to write reverted file: %1").arg(m_filePath)); - return; - } - - finishWithSuccess(EditStatus::Reverted, QString("Successfully reverted edit to: %1").arg(m_filePath)); -} - -void FileEditItem::rejectWithError(const QString &errorMessage) -{ - setStatus(EditStatus::Rejected); - setStatusMessage(errorMessage); -} - -void FileEditItem::finishWithSuccess(EditStatus status, const QString &message) -{ - setStatus(status); - setStatusMessage(message); -} - -void FileEditItem::setStatus(EditStatus status) -{ - if (m_status == status) - return; - - m_status = status; - emit statusChanged(); -} - -void FileEditItem::setStatusMessage(const QString &message) -{ - if (m_statusMessage == message) - return; - - m_statusMessage = message; - emit statusMessageChanged(); -} - -bool FileEditItem::writeFile(const QString &filePath, const QString &content) -{ - QFile file(filePath); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - return false; - } - - QTextStream stream(&file); - stream.setAutoDetectUnicode(true); - stream << content; - file.close(); - - if (stream.status() != QTextStream::Ok) { - return false; - } - - return true; -} - -QString FileEditItem::readFile(const QString &filePath) -{ - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - return QString(); - } - - QTextStream stream(&file); - stream.setAutoDetectUnicode(true); - QString content = stream.readAll(); - file.close(); - - return content; -} - -bool FileEditItem::acquireFileLock(const QString &filePath) -{ - QMutexLocker locker(&s_fileLockMutex); - - if (s_lockedFiles.contains(filePath)) { - return false; - } - - s_lockedFiles.insert(filePath); - return true; -} - -void FileEditItem::releaseFileLock(const QString &filePath) -{ - QMutexLocker locker(&s_fileLockMutex); - s_lockedFiles.remove(filePath); -} - -} // namespace QodeAssist::Chat diff --git a/ChatView/FileEditItem.hpp b/ChatView/FileEditItem.hpp deleted file mode 100644 index 6a0fb0e..0000000 --- a/ChatView/FileEditItem.hpp +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2025 Petr Mironychev - * - * This file is part of QodeAssist. - * - * QodeAssist is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QodeAssist is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QodeAssist. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include - -namespace QodeAssist::Chat { - -class FileEditItem : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - -public: - enum class EditStatus { - Pending, - Applied, - Rejected, - Reverted - }; - Q_ENUM(EditStatus) - - static constexpr const char *EDIT_MARKER = "QODEASSIST_FILE_EDIT:"; - static constexpr int RETRY_DELAY_MS = 100; - static constexpr int AUTO_APPLY_RETRY_DELAY_MS = 50; - static constexpr int MAX_RETRY_COUNT = 10; - - Q_PROPERTY(QString editId READ editId NOTIFY editIdChanged FINAL) - Q_PROPERTY(QString mode READ mode NOTIFY modeChanged FINAL) - Q_PROPERTY(QString filePath READ filePath NOTIFY filePathChanged FINAL) - Q_PROPERTY(QString searchText READ searchText NOTIFY searchTextChanged FINAL) - Q_PROPERTY(QString newText READ newText NOTIFY newTextChanged FINAL) - Q_PROPERTY(QString lineBefore READ lineBefore NOTIFY lineBeforeChanged FINAL) - Q_PROPERTY(QString lineAfter READ lineAfter NOTIFY lineAfterChanged FINAL) - Q_PROPERTY(int addedLines READ addedLines NOTIFY addedLinesChanged FINAL) - Q_PROPERTY(int removedLines READ removedLines NOTIFY removedLinesChanged FINAL) - Q_PROPERTY(EditStatus status READ status NOTIFY statusChanged FINAL) - Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged FINAL) - -public: - explicit FileEditItem(QQuickItem *parent = nullptr); - - QString editId() const { return m_editId; } - QString mode() const { return m_mode; } - QString filePath() const { return m_filePath; } - QString searchText() const { return m_searchText; } - QString newText() const { return m_newText; } - QString lineBefore() const { return m_lineBefore; } - QString lineAfter() const { return m_lineAfter; } - int addedLines() const { return m_addedLines; } - int removedLines() const { return m_removedLines; } - EditStatus status() const { return m_status; } - QString statusMessage() const { return m_statusMessage; } - - Q_INVOKABLE void parseFromContent(const QString &content); - Q_INVOKABLE void applyEdit(); - Q_INVOKABLE void revertEdit(); - -signals: - void editIdChanged(); - void modeChanged(); - void filePathChanged(); - void searchTextChanged(); - void newTextChanged(); - void lineBeforeChanged(); - void lineAfterChanged(); - void addedLinesChanged(); - void removedLinesChanged(); - void statusChanged(); - void statusMessageChanged(); - -private: - void setStatus(EditStatus status); - void setStatusMessage(const QString &message); - void applyEditInternal(bool isAutomatic, int retryCount = 0); - void performApply(); - void performRevert(); - void rejectWithError(const QString &errorMessage); - void finishWithSuccess(EditStatus status, const QString &message); - - bool writeFile(const QString &filePath, const QString &content); - QString readFile(const QString &filePath); - - static bool acquireFileLock(const QString &filePath); - static void releaseFileLock(const QString &filePath); - static QMutex s_fileLockMutex; - static QSet s_lockedFiles; - - QString m_editId; - QString m_mode; - QString m_filePath; - QString m_searchText; - QString m_newText; - QString m_lineBefore; - QString m_lineAfter; - QString m_originalContent; // Stored when applying edit - int m_addedLines = 0; - int m_removedLines = 0; - EditStatus m_status = EditStatus::Pending; - QString m_statusMessage; -}; - -} // namespace QodeAssist::Chat - diff --git a/ChatView/qml/FileEditChangesItem.qml b/ChatView/qml/FileEditChangesItem.qml deleted file mode 100644 index fab0a5e..0000000 --- a/ChatView/qml/FileEditChangesItem.qml +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2025 Petr Mironychev - * - * This file is part of QodeAssist. - * - * QodeAssist is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QodeAssist is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QodeAssist. If not, see . - */ - -import QtQuick -import QtQuick.Layouts -import ChatView -import UIControls -import "./parts" - -FileEditItem { - id: root - - implicitHeight: fileEditView.implicitHeight - - Component.onCompleted: { - root.parseFromContent(model.content) - } - - readonly property int borderRadius: 4 - readonly property int contentMargin: 10 - readonly property int contentBottomPadding: 20 - readonly property int headerPadding: 8 - readonly property int statusIndicatorWidth: 4 - - readonly property bool isPending: status === FileEditItem.Pending - readonly property bool isApplied: status === FileEditItem.Applied - readonly property bool isReverted: status === FileEditItem.Reverted - readonly property bool isRejected: status === FileEditItem.Rejected - - readonly property color appliedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8) - readonly property color revertedColor: Qt.rgba(0.8, 0.6, 0.2, 0.8) - readonly property color rejectedColor: palette.mid - readonly property color pendingColor: palette.highlight - - readonly property color appliedBgColor: Qt.rgba(0.2, 0.8, 0.2, 0.3) - readonly property color revertedBgColor: Qt.rgba(0.8, 0.6, 0.2, 0.3) - readonly property color rejectedBgColor: Qt.rgba(0.8, 0.2, 0.2, 0.3) - - readonly property string codeFontFamily: { - switch (Qt.platform.os) { - case "windows": return "Consolas" - case "osx": return "Menlo" - case "linux": return "DejaVu Sans Mono" - default: return "monospace" - } - } - readonly property int codeFontSize: Qt.application.font.pointSize - - readonly property color statusColor: { - if (isApplied) return appliedColor - if (isReverted) return revertedColor - if (isRejected) return rejectedColor - return pendingColor - } - - readonly property color statusBgColor: { - if (isApplied) return appliedBgColor - if (isReverted) return revertedBgColor - if (isRejected) return rejectedBgColor - return palette.button - } - - readonly property string statusText: { - if (isApplied) return qsTr("APPLIED") - if (isReverted) return qsTr("REVERTED") - if (isRejected) return qsTr("REJECTED") - return "" - } - - Rectangle { - id: fileEditView - - property bool expanded: false - - anchors.fill: parent - implicitHeight: expanded ? headerArea.height + contentColumn.height + root.contentBottomPadding - : headerArea.height - radius: root.borderRadius - - color: palette.base - - border.width: 1 - border.color: root.isPending - ? (color.hslLightness > 0.5 ? Qt.darker(color, 1.3) : Qt.lighter(color, 1.3)) - : Qt.alpha(root.statusColor, 0.6) - - clip: true - - states: [ - State { - name: "expanded" - when: fileEditView.expanded - PropertyChanges { target: contentColumn; opacity: 1 } - }, - State { - name: "collapsed" - when: !fileEditView.expanded - PropertyChanges { target: contentColumn; opacity: 0 } - } - ] - - transitions: Transition { - NumberAnimation { - properties: "implicitHeight,opacity" - duration: 200 - easing.type: Easing.InOutQuad - } - } - - MouseArea { - id: headerArea - width: parent.width - height: headerRow.height + 16 - cursorShape: Qt.PointingHandCursor - onClicked: fileEditView.expanded = !fileEditView.expanded - - RowLayout { - id: headerRow - - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - right: actionButtons.left - leftMargin: root.contentMargin - rightMargin: root.contentMargin - } - spacing: root.headerPadding - - Rectangle { - width: root.statusIndicatorWidth - height: headerText.height - radius: 2 - color: root.statusColor - } - - Text { - id: headerText - Layout.fillWidth: true - text: { - var modeText = root.searchText.length > 0 ? qsTr("Replace") : qsTr("Append") - return qsTr("%1: %2 (+%3 -%4)") - .arg(modeText) - .arg(root.filePath) - .arg(root.addedLines) - .arg(root.removedLines) - } - font.pixelSize: 12 - font.bold: true - color: palette.text - elide: Text.ElideMiddle - } - - Text { - text: fileEditView.expanded ? "▼" : "▶" - font.pixelSize: 10 - color: palette.mid - } - - Badge { - visible: !root.isPending - text: root.statusText - color: root.statusBgColor - } - } - - Row { - id: actionButtons - - anchors { - right: parent.right - rightMargin: 5 - verticalCenter: parent.verticalCenter - } - spacing: 6 - - QoAButton { - text: qsTr("Apply") - enabled: root.isPending || root.isReverted || root.isRejected - visible: !root.isApplied - onClicked: root.applyEdit() - } - - QoAButton { - text: qsTr("Revert") - enabled: root.isApplied - visible: !root.isReverted && !root.isRejected - onClicked: root.revertEdit() - } - } - } - - ColumnLayout { - id: contentColumn - - anchors { - left: parent.left - right: parent.right - top: headerArea.bottom - margins: root.contentMargin - } - spacing: 4 - visible: opacity > 0 - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: oldContentText.implicitHeight + 8 - color: Qt.rgba(1, 0.2, 0.2, 0.1) - radius: 4 - border.width: 1 - border.color: Qt.rgba(1, 0.2, 0.2, 0.3) - visible: root.searchText.length > 0 - - TextEdit { - id: oldContentText - anchors { - left: parent.left - right: parent.right - top: parent.top - margins: 4 - } - text: root.searchText - font.family: root.codeFontFamily - font.pixelSize: root.codeFontSize - color: Qt.rgba(1, 0.2, 0.2, 0.9) - wrapMode: TextEdit.Wrap - readOnly: true - selectByMouse: true - selectByKeyboard: true - textFormat: TextEdit.PlainText - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: newContentText.implicitHeight + 8 - color: Qt.rgba(0.2, 0.8, 0.2, 0.1) - radius: 4 - border.width: 1 - border.color: Qt.rgba(0.2, 0.8, 0.2, 0.3) - - TextEdit { - id: newContentText - anchors { - left: parent.left - right: parent.right - top: parent.top - margins: 4 - } - text: root.newText - font.family: root.codeFontFamily - font.pixelSize: root.codeFontSize - color: Qt.rgba(0.2, 0.8, 0.2, 0.9) - wrapMode: TextEdit.Wrap - readOnly: true - selectByMouse: true - selectByKeyboard: true - textFormat: TextEdit.PlainText - } - } - - Text { - Layout.fillWidth: true - visible: root.statusMessage.length > 0 - text: root.statusMessage - font.pixelSize: 11 - font.italic: true - color: root.isApplied - ? Qt.rgba(0.2, 0.6, 0.2, 1) - : Qt.rgba(0.8, 0.2, 0.2, 1) - wrapMode: Text.WordWrap - } - } - } -} diff --git a/tools/EditFileTool.cpp b/tools/EditFileTool.cpp deleted file mode 100644 index 13439a9..0000000 --- a/tools/EditFileTool.cpp +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright (C) 2025 Petr Mironychev - * - * This file is part of QodeAssist. - * - * QodeAssist is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QodeAssist is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QodeAssist. If not, see . - */ - -#include "EditFileTool.hpp" -#include "ToolExceptions.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Tools { - -EditFileTool::EditFileTool(QObject *parent) - : BaseTool(parent) - , m_ignoreManager(new Context::IgnoreManager(this)) -{} - -QString EditFileTool::name() const -{ - return "edit_file"; -} - -QString EditFileTool::stringName() const -{ - return {"Editing file"}; -} - -QString EditFileTool::description() const -{ - return "Edit project files with two modes: REPLACE (find and replace text) or INSERT_AFTER " - "(insert after specific line). All text parameters must be complete lines with trailing " - "newlines (\\n auto-added if missing).\n" - "\n" - "REPLACE MODE:\n" - "- Finds search_text and replaces with new_text\n" - "- Context verification: line_before/line_after searched NEAR search_text (~500 chars), " - "not necessarily adjacent\n" - "- Both context lines: most precise matching\n" - "- One context line: directional search (before/after)\n" - "- No context: first occurrence\n" - "\n" - "INSERT_AFTER MODE:\n" - "- Inserts new_text RIGHT AFTER line_before\n" - "- Empty line_before: inserts at file start (useful for empty files)\n" - "- line_after: must IMMEDIATELY follow line_before for verification\n" - "- search_text is ignored\n" - "\n" - "BEST PRACTICES:\n" - "- Sequential additions: use INSERT_AFTER with previous addition as line_before\n" - "- Provide stable context lines that won't change\n" - "- Make atomic edits (one comprehensive change vs multiple small ones)"; -} - -QJsonObject EditFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const -{ - QJsonObject properties; - - QJsonObject modeProperty; - modeProperty["type"] = "string"; - modeProperty["enum"] = QJsonArray({"replace", "insert_after"}); - modeProperty["description"] = "Edit mode: 'replace' (replace search_text with new_text), " - "'insert_after' (insert new_text after line_before)"; - properties["mode"] = modeProperty; - - QJsonObject filepathProperty; - filepathProperty["type"] = "string"; - filepathProperty["description"] = "The absolute or relative file path to edit"; - properties["filepath"] = filepathProperty; - - QJsonObject newTextProperty; - newTextProperty["type"] = "string"; - newTextProperty["description"] = "Complete line(s) to insert/replace/append. Trailing newline (\\n) auto-added if missing. " - "Example: 'int main(int argc, char *argv[]) {\\n' or 'void foo();\\nvoid bar();\\n'"; - properties["new_text"] = newTextProperty; - - QJsonObject searchTextProperty; - searchTextProperty["type"] = "string"; - searchTextProperty["description"] - = "Complete line(s) to search for and replace. Trailing newline (\\n) auto-added if missing. " - "REQUIRED for 'replace' mode, IGNORED for other modes. " - "Example: 'int main() {\\n' or 'void foo();\\n'"; - properties["search_text"] = searchTextProperty; - - QJsonObject lineBeforeProperty; - lineBeforeProperty["type"] = "string"; - lineBeforeProperty["description"] = "Complete line for context verification. Trailing newline (\\n) auto-added if missing. " - "Usage depends on mode:\n" - "- 'replace': OPTIONAL, searched BEFORE search_text (within ~500 chars, not necessarily adjacent)\n" - "- 'insert_after': OPTIONAL, new_text inserted RIGHT AFTER this line. " - "If empty, inserts at the beginning of the file (useful for empty files)\n" - "Example: 'class Movie {\\n' or '#include \\n'"; - properties["line_before"] = lineBeforeProperty; - - QJsonObject lineAfterProperty; - lineAfterProperty["type"] = "string"; - lineAfterProperty["description"] = "Complete line for context verification. Trailing newline (\\n) auto-added if missing. " - "Usage depends on mode:\n" - "- 'replace': OPTIONAL, searched AFTER search_text (within ~500 chars, not necessarily adjacent)\n" - "- 'insert_after': OPTIONAL, must IMMEDIATELY follow line_before for verification\n" - "Example: '}\\n' or 'public:\\n'"; - properties["line_after"] = lineAfterProperty; - - QJsonObject definition; - definition["type"] = "object"; - definition["properties"] = properties; - - QJsonArray required; - required.append("mode"); - required.append("filepath"); - required.append("new_text"); - definition["required"] = required; - - switch (format) { - case LLMCore::ToolSchemaFormat::OpenAI: - return customizeForOpenAI(definition); - case LLMCore::ToolSchemaFormat::Claude: - return customizeForClaude(definition); - case LLMCore::ToolSchemaFormat::Ollama: - return customizeForOllama(definition); - case LLMCore::ToolSchemaFormat::Google: - return customizeForGoogle(definition); - } - - return definition; -} - -LLMCore::ToolPermissions EditFileTool::requiredPermissions() const -{ - return LLMCore::ToolPermissions(LLMCore::ToolPermission::FileSystemRead) - | LLMCore::ToolPermissions(LLMCore::ToolPermission::FileSystemWrite); -} - -QFuture EditFileTool::executeAsync(const QJsonObject &input) -{ - return QtConcurrent::run([this, input]() -> QString { - QString mode = input["mode"].toString(); - if (mode.isEmpty()) { - throw ToolInvalidArgument("Error: mode parameter is required. Must be one of: 'replace', 'insert_after'"); - } - - if (mode != "replace" && mode != "insert_after") { - throw ToolInvalidArgument(QString("Error: invalid mode '%1'. Must be one of: 'replace', 'insert_after'").arg(mode)); - } - - QString inputFilepath = input["filepath"].toString(); - if (inputFilepath.isEmpty()) { - throw ToolInvalidArgument("Error: filepath parameter is required"); - } - - QString newText = input["new_text"].toString(); - if (newText.isEmpty()) { - throw ToolInvalidArgument("Error: new_text parameter is required"); - } - - QString searchText = input["search_text"].toString(); - QString lineBefore = input["line_before"].toString(); - QString lineAfter = input["line_after"].toString(); - - if (mode == "replace" && searchText.isEmpty()) { - throw ToolInvalidArgument("Error: search_text is required for 'replace' mode"); - } - - // Normalize text fields: ensure trailing newline if not empty - // This handles cases where LLM forgets to add \n - auto normalizeText = [](QString &text) { - if (!text.isEmpty() && !text.endsWith('\n')) { - LOG_MESSAGE(QString("EditFileTool: normalizing text, adding trailing newline (length: %1)").arg(text.length())); - text += '\n'; - } - }; - - normalizeText(newText); - if (!searchText.isEmpty()) normalizeText(searchText); - if (!lineBefore.isEmpty()) normalizeText(lineBefore); - if (!lineAfter.isEmpty()) normalizeText(lineAfter); - - QString filePath; - QFileInfo fileInfo(inputFilepath); - - if (fileInfo.isAbsolute()) { - filePath = inputFilepath; - } else { - auto projects = ProjectExplorer::ProjectManager::projects(); - if (!projects.isEmpty() && projects.first()) { - QString projectDir = projects.first()->projectDirectory().toUrlishString(); - filePath = QDir(projectDir).absoluteFilePath(inputFilepath); - } else { - filePath = QFileInfo(inputFilepath).absoluteFilePath(); - } - } - - if (!QFileInfo::exists(filePath)) { - throw ToolRuntimeError(QString("Error: File '%1' does not exist").arg(filePath)); - } - - bool isInProject = Context::ProjectUtils::isFileInProject(filePath); - - if (!isInProject) { - const auto &settings = Settings::generalSettings(); - if (!settings.allowAccessOutsideProject()) { - throw ToolRuntimeError( - QString("Error: File '%1' is outside the project scope. " - "Enable 'Allow file access outside project' in settings to edit files outside project scope.") - .arg(filePath)); - } - LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath)); - } - - auto project = isInProject ? ProjectExplorer::ProjectManager::projectForFile( - Utils::FilePath::fromString(filePath)) : nullptr; - - if (project && m_ignoreManager->shouldIgnore(filePath, project)) { - throw ToolRuntimeError( - QString("Error: File '%1' is excluded by .qodeassistignore").arg(inputFilepath)); - } - - QJsonObject result; - result["type"] = "file_edit"; - result["mode"] = mode; - result["filepath"] = filePath; - result["new_text"] = newText; - result["search_text"] = searchText; - result["line_before"] = lineBefore; - result["line_after"] = lineAfter; - - QJsonDocument doc(result); - return QString("QODEASSIST_FILE_EDIT:%1") - .arg(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); - }); -} - -} // namespace QodeAssist::Tools - diff --git a/tools/EditFileTool.hpp b/tools/EditFileTool.hpp deleted file mode 100644 index f25c63d..0000000 --- a/tools/EditFileTool.hpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2025 Petr Mironychev - * - * This file is part of QodeAssist. - * - * QodeAssist is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QodeAssist is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QodeAssist. If not, see . - */ - -#pragma once - -#include -#include - -namespace QodeAssist::Tools { - -class EditFileTool : public LLMCore::BaseTool -{ - Q_OBJECT -public: - explicit EditFileTool(QObject *parent = nullptr); - - QString name() const override; - QString stringName() const override; - QString description() const override; - QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override; - LLMCore::ToolPermissions requiredPermissions() const override; - - QFuture executeAsync(const QJsonObject &input = QJsonObject()) override; - -private: - Context::IgnoreManager *m_ignoreManager; -}; - -} // namespace QodeAssist::Tools - diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp index 90eb80f..613556b 100644 --- a/tools/ToolsFactory.cpp +++ b/tools/ToolsFactory.cpp @@ -25,7 +25,6 @@ #include #include "CreateNewFileTool.hpp" -#include "EditFileTool.hpp" #include "FindFileTool.hpp" #include "FindSymbolTool.hpp" #include "GetIssuesListTool.hpp" @@ -49,7 +48,6 @@ void ToolsFactory::registerTools() registerTool(new ListProjectFilesTool(this)); registerTool(new SearchInProjectTool(this)); registerTool(new GetIssuesListTool(this)); - registerTool(new EditFileTool(this)); registerTool(new FindSymbolTool(this)); registerTool(new FindFileTool(this)); registerTool(new CreateNewFileTool(this));