mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-20 10:10:56 -04:00
refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore
This commit is contained in:
224
sources/tools/EditFileTool.cpp
Normal file
224
sources/tools/EditFileTool.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
// 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 "EditFileTool.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <context/ChangesManager.h>
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
EditFileTool::EditFileTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{}
|
||||
|
||||
QString EditFileTool::id() const
|
||||
{
|
||||
return "edit_file";
|
||||
}
|
||||
|
||||
QString EditFileTool::displayName() const
|
||||
{
|
||||
return {"Editing file"};
|
||||
}
|
||||
|
||||
QString EditFileTool::description() const
|
||||
{
|
||||
return "Edit a file by replacing old content with new content. "
|
||||
"Provide the file path (absolute or relative to project root), old_content to find and replace, "
|
||||
"and new_content to replace it with. Changes are applied immediately if auto-apply "
|
||||
"is enabled in settings. The user can undo or reapply changes at any time. "
|
||||
"\n\nIMPORTANT:"
|
||||
"\n- ALWAYS use read_file to get current file content before editing to ensure accuracy."
|
||||
"\n- Path can be absolute (e.g., /path/to/file.cpp) or relative to project root (e.g., src/main.cpp)."
|
||||
"\n- For EMPTY files: use empty old_content (empty string or omit parameter)."
|
||||
"\n- To append at the END of file: use empty old_content."
|
||||
"\n- To insert at the BEGINNING of a file (e.g., copyright header), you MUST provide "
|
||||
"the EXACT first few lines of the file as old_content (at least 3-5 lines), "
|
||||
"then put those lines + new header in new_content."
|
||||
"\n- For replacements in the middle, provide EXACT matching text with sufficient "
|
||||
"context (at least 5-10 lines) to ensure correct placement."
|
||||
"\n- The system uses fuzzy matching with 85% similarity threshold for first-time edits. "
|
||||
"Provide accurate old_content to avoid incorrect placement."
|
||||
"\n- If changes remain 'pending' and file content hasn't changed, the user likely "
|
||||
"disabled auto-apply. DO NOT retry the same edit - wait for user action.";
|
||||
}
|
||||
|
||||
QJsonObject EditFileTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
QJsonObject filenameProperty;
|
||||
filenameProperty["type"] = "string";
|
||||
filenameProperty["description"]
|
||||
= "The path of the file to edit. Can be an absolute path (e.g., /path/to/file.cpp) "
|
||||
"or a relative path from the project root (e.g., src/main.cpp)";
|
||||
properties["filename"] = filenameProperty;
|
||||
|
||||
QJsonObject oldContentProperty;
|
||||
oldContentProperty["type"] = "string";
|
||||
oldContentProperty["description"]
|
||||
= "The content to find and replace. For exact matches, provide precise text "
|
||||
"(including whitespace). For changed files, the system uses fuzzy matching with "
|
||||
"85% similarity threshold for first-time edits. If empty, new_content will be "
|
||||
"appended to the end of the file";
|
||||
properties["old_content"] = oldContentProperty;
|
||||
|
||||
QJsonObject newContentProperty;
|
||||
newContentProperty["type"] = "string";
|
||||
newContentProperty["description"] = "The new content to replace the old content with";
|
||||
properties["new_content"] = newContentProperty;
|
||||
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = properties;
|
||||
QJsonArray required;
|
||||
required.append("filename");
|
||||
required.append("new_content");
|
||||
definition["required"] = required;
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> EditFileTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> LLMQore::ToolResult {
|
||||
QString filename = input["filename"].toString().trimmed();
|
||||
QString oldContent = input["old_content"].toString();
|
||||
QString newContent = input["new_content"].toString();
|
||||
QString requestId = input["_request_id"].toString();
|
||||
|
||||
if (filename.isEmpty()) {
|
||||
throw LLMQore::ToolInvalidArgument("'filename' parameter is required and cannot be empty");
|
||||
}
|
||||
|
||||
if (newContent.isEmpty()) {
|
||||
throw LLMQore::ToolInvalidArgument("'new_content' parameter is required and cannot be empty");
|
||||
}
|
||||
|
||||
|
||||
QFileInfo fileInfo(filename);
|
||||
QString filePath;
|
||||
|
||||
if (fileInfo.isAbsolute()) {
|
||||
filePath = filename;
|
||||
} else {
|
||||
QString projectRoot = Context::ProjectUtils::getProjectRoot();
|
||||
if (projectRoot.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Cannot resolve relative path '%1': no project is open. "
|
||||
"Please provide an absolute path or open a project.")
|
||||
.arg(filename));
|
||||
}
|
||||
|
||||
filePath = QDir(projectRoot).absoluteFilePath(filename);
|
||||
LOG_MESSAGE(QString("EditFileTool: Resolved relative path '%1' to '%2'")
|
||||
.arg(filename, filePath));
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.exists()) {
|
||||
throw LLMQore::ToolRuntimeError(QString("File does not exist: %1").arg(filePath));
|
||||
}
|
||||
|
||||
QFileInfo finalFileInfo(filePath);
|
||||
if (!finalFileInfo.isWritable()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("File is not writable (read-only or permission denied): %1").arg(filePath));
|
||||
}
|
||||
|
||||
bool isInProject = Context::ProjectUtils::isFileInProject(filePath);
|
||||
if (!isInProject) {
|
||||
const auto &settings = Settings::toolsSettings();
|
||||
if (!settings.allowAccessOutsideProject()) {
|
||||
const QString projectRoot = Context::ProjectUtils::getProjectRoot();
|
||||
const QString hint = projectRoot.isEmpty()
|
||||
? QStringLiteral(
|
||||
"No project is currently open. Open a project in Qt Creator or "
|
||||
"enable 'Allow file access outside project' in QodeAssist settings.")
|
||||
: QString(
|
||||
"Retry with a path under the active project root: '%1'. The build "
|
||||
"directory is for compiler output only — source files must live under "
|
||||
"the project root. If you really need to edit outside the project, "
|
||||
"enable 'Allow file access outside project' in QodeAssist settings.")
|
||||
.arg(projectRoot);
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("File path '%1' is not within the current project. %2")
|
||||
.arg(filePath, hint));
|
||||
}
|
||||
LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath));
|
||||
}
|
||||
|
||||
QString editId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
bool autoApply = Settings::toolsSettings().autoApplyFileEdits();
|
||||
|
||||
LOG_MESSAGE(QString("EditFileTool: Edit details for %1:").arg(filePath));
|
||||
LOG_MESSAGE(QString(" oldContent length: %1 chars").arg(oldContent.length()));
|
||||
LOG_MESSAGE(QString(" newContent length: %1 chars").arg(newContent.length()));
|
||||
if (oldContent.length() <= 200) {
|
||||
LOG_MESSAGE(QString(" oldContent: '%1'").arg(oldContent));
|
||||
} else {
|
||||
LOG_MESSAGE(QString(" oldContent (first 200 chars): '%1...'")
|
||||
.arg(oldContent.left(200)));
|
||||
}
|
||||
if (newContent.length() <= 200) {
|
||||
LOG_MESSAGE(QString(" newContent: '%1'").arg(newContent));
|
||||
} else {
|
||||
LOG_MESSAGE(QString(" newContent (first 200 chars): '%1...'")
|
||||
.arg(newContent.left(200)));
|
||||
}
|
||||
|
||||
Context::ChangesManager::instance().addFileEdit(
|
||||
editId,
|
||||
filePath,
|
||||
oldContent,
|
||||
newContent,
|
||||
autoApply,
|
||||
false,
|
||||
requestId
|
||||
);
|
||||
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
QString status = "pending";
|
||||
if (edit.status == Context::ChangesManager::Applied) {
|
||||
status = "applied";
|
||||
} else if (edit.status == Context::ChangesManager::Rejected) {
|
||||
status = "rejected";
|
||||
} else if (edit.status == Context::ChangesManager::Archived) {
|
||||
status = "archived";
|
||||
}
|
||||
|
||||
QString statusMessage = edit.statusMessage;
|
||||
|
||||
QJsonObject result;
|
||||
result["edit_id"] = editId;
|
||||
result["file"] = filePath;
|
||||
result["old_content"] = oldContent;
|
||||
result["new_content"] = newContent;
|
||||
result["status"] = status;
|
||||
result["status_message"] = statusMessage;
|
||||
|
||||
LOG_MESSAGE(QString("File edit created: %1 (ID: %2, Status: %3, Deferred: %4)")
|
||||
.arg(filePath, editId, status, requestId.isEmpty() ? QString("no") : QString("yes")));
|
||||
|
||||
QString resultStr = "QODEASSIST_FILE_EDIT:"
|
||||
+ QString::fromUtf8(QJsonDocument(result).toJson(QJsonDocument::Compact));
|
||||
return LLMQore::ToolResult::text(resultStr);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
Reference in New Issue
Block a user