mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-11-12 21:12:44 -05:00
feat: Add edit file tool (#249)
* feat: Add edit file tool * feat: Add icons for action buttons
This commit is contained in:
@ -49,7 +49,7 @@ QString BuildProjectTool::stringName() const
|
||||
QString BuildProjectTool::description() const
|
||||
{
|
||||
return "Build the current project in Qt Creator. "
|
||||
"Returns build status and any compilation errors/warnings. "
|
||||
"No returns simultaneously build status and any compilation errors/warnings. "
|
||||
"Optional 'rebuild' parameter: set to true to force a clean rebuild (default: false).";
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
@ -103,7 +104,7 @@ QFuture<QString> CreateNewFileTool::executeAsync(const QJsonObject &input)
|
||||
bool isInProject = Context::ProjectUtils::isFileInProject(absolutePath);
|
||||
|
||||
if (!isInProject) {
|
||||
const auto &settings = Settings::generalSettings();
|
||||
const auto &settings = Settings::toolsSettings();
|
||||
if (!settings.allowAccessOutsideProject()) {
|
||||
throw ToolRuntimeError(
|
||||
QString("Error: File path '%1' is not within the current project. "
|
||||
|
||||
215
tools/EditFileTool.cpp
Normal file
215
tools/EditFileTool.cpp
Normal file
@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "EditFileTool.hpp"
|
||||
#include "ToolExceptions.hpp"
|
||||
|
||||
#include <context/ChangesManager.h>
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QtConcurrent>
|
||||
|
||||
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 a file by replacing old content with new content. "
|
||||
"Provide the filename (or absolute path), 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. "
|
||||
"If old_content is empty, new_content will be appended to the end of the file.";
|
||||
}
|
||||
|
||||
QJsonObject EditFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
QJsonObject filenameProperty;
|
||||
filenameProperty["type"] = "string";
|
||||
filenameProperty["description"]
|
||||
= "The filename or absolute path of the file to edit. If only filename is provided, "
|
||||
"it will be searched in the project";
|
||||
properties["filename"] = filenameProperty;
|
||||
|
||||
QJsonObject oldContentProperty;
|
||||
oldContentProperty["type"] = "string";
|
||||
oldContentProperty["description"]
|
||||
= "The exact content to find and replace. Must match exactly (including whitespace). "
|
||||
"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;
|
||||
|
||||
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::ToolPermission::FileSystemWrite;
|
||||
}
|
||||
|
||||
QFuture<QString> EditFileTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> QString {
|
||||
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 ToolInvalidArgument("'filename' parameter is required and cannot be empty");
|
||||
}
|
||||
|
||||
if (newContent.isEmpty()) {
|
||||
throw ToolInvalidArgument("'new_content' parameter is required and cannot be empty");
|
||||
}
|
||||
|
||||
|
||||
QString filePath;
|
||||
QFileInfo fileInfo(filename);
|
||||
|
||||
if (fileInfo.isAbsolute() && fileInfo.exists()) {
|
||||
filePath = filename;
|
||||
} else {
|
||||
FileSearchUtils::FileMatch match = FileSearchUtils::findBestMatch(
|
||||
filename, QString(), 10, m_ignoreManager);
|
||||
|
||||
if (match.absolutePath.isEmpty()) {
|
||||
throw ToolRuntimeError(
|
||||
QString("File '%1' not found in project. "
|
||||
"Please provide a valid filename or absolute path.")
|
||||
.arg(filename));
|
||||
}
|
||||
|
||||
filePath = match.absolutePath;
|
||||
LOG_MESSAGE(QString("EditFileTool: Found file '%1' at '%2'")
|
||||
.arg(filename, filePath));
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.exists()) {
|
||||
throw ToolRuntimeError(QString("File does not exist: %1").arg(filePath));
|
||||
}
|
||||
|
||||
QFileInfo finalFileInfo(filePath);
|
||||
if (!finalFileInfo.isWritable()) {
|
||||
throw 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()) {
|
||||
throw ToolRuntimeError(
|
||||
QString("File path '%1' is not within the current project. "
|
||||
"Enable 'Allow file access outside project' in settings to edit files outside the project.")
|
||||
.arg(filePath));
|
||||
}
|
||||
LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath));
|
||||
}
|
||||
|
||||
QString editId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
bool autoApply = Settings::toolsSettings().autoApplyFileEdits();
|
||||
|
||||
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 resultStr;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
48
tools/EditFileTool.hpp
Normal file
48
tools/EditFileTool.hpp
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FileSearchUtils.hpp"
|
||||
|
||||
#include <context/IgnoreManager.hpp>
|
||||
#include <llmcore/BaseTool.hpp>
|
||||
|
||||
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<QString> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
private:
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
258
tools/FileSearchUtils.cpp
Normal file
258
tools/FileSearchUtils.cpp
Normal file
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FileSearchUtils.hpp"
|
||||
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTextStream>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
FileSearchUtils::FileMatch FileSearchUtils::findBestMatch(
|
||||
const QString &query,
|
||||
const QString &filePattern,
|
||||
int maxResults,
|
||||
Context::IgnoreManager *ignoreManager)
|
||||
{
|
||||
QList<FileMatch> candidates;
|
||||
auto projects = ProjectExplorer::ProjectManager::projects();
|
||||
|
||||
if (projects.isEmpty()) {
|
||||
return FileMatch{};
|
||||
}
|
||||
|
||||
QFileInfo queryInfo(query);
|
||||
if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) {
|
||||
FileMatch match;
|
||||
match.absolutePath = queryInfo.canonicalFilePath();
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
if (match.absolutePath.startsWith(projectDir)) {
|
||||
match.relativePath = QDir(projectDir).relativeFilePath(match.absolutePath);
|
||||
match.projectName = project->displayName();
|
||||
match.matchType = MatchType::ExactName;
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
match.relativePath = queryInfo.fileName();
|
||||
match.projectName = "External";
|
||||
match.matchType = MatchType::ExactName;
|
||||
return match;
|
||||
}
|
||||
|
||||
QString lowerQuery = query.toLower();
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
QString projectName = project->displayName();
|
||||
|
||||
for (const auto &filePath : projectFiles) {
|
||||
QString absolutePath = filePath.path();
|
||||
|
||||
if (ignoreManager && ignoreManager->shouldIgnore(absolutePath, project))
|
||||
continue;
|
||||
|
||||
QFileInfo fileInfo(absolutePath);
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
if (!filePattern.isEmpty() && !matchesFilePattern(fileName, filePattern))
|
||||
continue;
|
||||
|
||||
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||
|
||||
FileMatch match;
|
||||
match.absolutePath = absolutePath;
|
||||
match.relativePath = relativePath;
|
||||
match.projectName = projectName;
|
||||
|
||||
QString lowerFileName = fileName.toLower();
|
||||
QString lowerRelativePath = relativePath.toLower();
|
||||
|
||||
if (lowerFileName == lowerQuery) {
|
||||
match.matchType = MatchType::ExactName;
|
||||
candidates.append(match);
|
||||
} else if (lowerRelativePath.contains(lowerQuery)) {
|
||||
match.matchType = MatchType::PathMatch;
|
||||
candidates.append(match);
|
||||
} else if (lowerFileName.contains(lowerQuery)) {
|
||||
match.matchType = MatchType::PartialName;
|
||||
candidates.append(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isEmpty() || candidates.first().matchType != MatchType::ExactName) {
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
QString projectName = project->displayName();
|
||||
int depth = 0;
|
||||
searchInFileSystem(
|
||||
projectDir,
|
||||
lowerQuery,
|
||||
projectName,
|
||||
projectDir,
|
||||
project,
|
||||
candidates,
|
||||
maxResults,
|
||||
depth,
|
||||
5,
|
||||
ignoreManager);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
return FileMatch{};
|
||||
}
|
||||
|
||||
std::sort(candidates.begin(), candidates.end());
|
||||
return candidates.first();
|
||||
}
|
||||
|
||||
void FileSearchUtils::searchInFileSystem(
|
||||
const QString &dirPath,
|
||||
const QString &query,
|
||||
const QString &projectName,
|
||||
const QString &projectDir,
|
||||
ProjectExplorer::Project *project,
|
||||
QList<FileMatch> &matches,
|
||||
int maxResults,
|
||||
int ¤tDepth,
|
||||
int maxDepth,
|
||||
Context::IgnoreManager *ignoreManager)
|
||||
{
|
||||
if (currentDepth >= maxDepth || matches.size() >= maxResults)
|
||||
return;
|
||||
|
||||
currentDepth++;
|
||||
QDir dir(dirPath);
|
||||
if (!dir.exists()) {
|
||||
currentDepth--;
|
||||
return;
|
||||
}
|
||||
|
||||
auto entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const auto &entry : entries) {
|
||||
if (matches.size() >= maxResults)
|
||||
break;
|
||||
|
||||
QString absolutePath = entry.absoluteFilePath();
|
||||
|
||||
if (ignoreManager && ignoreManager->shouldIgnore(absolutePath, project))
|
||||
continue;
|
||||
|
||||
QString fileName = entry.fileName();
|
||||
|
||||
if (entry.isDir()) {
|
||||
searchInFileSystem(
|
||||
absolutePath,
|
||||
query,
|
||||
projectName,
|
||||
projectDir,
|
||||
project,
|
||||
matches,
|
||||
maxResults,
|
||||
currentDepth,
|
||||
maxDepth,
|
||||
ignoreManager);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString lowerFileName = fileName.toLower();
|
||||
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||
QString lowerRelativePath = relativePath.toLower();
|
||||
|
||||
FileMatch match;
|
||||
match.absolutePath = absolutePath;
|
||||
match.relativePath = relativePath;
|
||||
match.projectName = projectName;
|
||||
|
||||
if (lowerFileName == query) {
|
||||
match.matchType = MatchType::ExactName;
|
||||
matches.append(match);
|
||||
} else if (lowerRelativePath.contains(query)) {
|
||||
match.matchType = MatchType::PathMatch;
|
||||
matches.append(match);
|
||||
} else if (lowerFileName.contains(query)) {
|
||||
match.matchType = MatchType::PartialName;
|
||||
matches.append(match);
|
||||
}
|
||||
}
|
||||
|
||||
currentDepth--;
|
||||
}
|
||||
|
||||
bool FileSearchUtils::matchesFilePattern(const QString &fileName, const QString &pattern)
|
||||
{
|
||||
if (pattern.isEmpty())
|
||||
return true;
|
||||
|
||||
if (pattern.startsWith("*.")) {
|
||||
QString extension = pattern.mid(1);
|
||||
return fileName.endsWith(extension, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return fileName.compare(pattern, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
QString FileSearchUtils::readFileContent(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString canonicalPath = QFileInfo(filePath).canonicalFilePath();
|
||||
bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath);
|
||||
|
||||
if (!isInProject) {
|
||||
const auto &settings = Settings::toolsSettings();
|
||||
if (!settings.allowAccessOutsideProject()) {
|
||||
LOG_MESSAGE(QString("Access denied to file outside project: %1").arg(canonicalPath));
|
||||
return QString();
|
||||
}
|
||||
LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath));
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setAutoDetectUnicode(true);
|
||||
QString content = stream.readAll();
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
152
tools/FileSearchUtils.hpp
Normal file
152
tools/FileSearchUtils.hpp
Normal file
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <context/IgnoreManager.hpp>
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
namespace ProjectExplorer {
|
||||
class Project;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
/**
|
||||
* @brief Utility class for file searching and reading operations
|
||||
*
|
||||
* Provides common functionality for file operations used by various tools:
|
||||
* - Fuzzy file searching with multiple match strategies
|
||||
* - File pattern matching (e.g., *.cpp, *.h)
|
||||
* - Secure file content reading with project boundary checks
|
||||
* - Integration with IgnoreManager for respecting .qodeassistignore
|
||||
*/
|
||||
class FileSearchUtils
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Match quality levels for file search results
|
||||
*/
|
||||
enum class MatchType {
|
||||
ExactName, ///< Exact filename match (highest priority)
|
||||
PathMatch, ///< Query found in relative path
|
||||
PartialName ///< Query found in filename (lowest priority)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a file search result with metadata
|
||||
*/
|
||||
struct FileMatch
|
||||
{
|
||||
QString absolutePath; ///< Full absolute path to the file
|
||||
QString relativePath; ///< Path relative to project root
|
||||
QString projectName; ///< Name of the project containing the file
|
||||
QString content; ///< File content (if read)
|
||||
MatchType matchType; ///< Quality of the match
|
||||
bool contentRead = false; ///< Whether content has been read
|
||||
QString error; ///< Error message if operation failed
|
||||
|
||||
/**
|
||||
* @brief Compare matches by quality (for sorting)
|
||||
*/
|
||||
bool operator<(const FileMatch &other) const
|
||||
{
|
||||
return static_cast<int>(matchType) < static_cast<int>(other.matchType);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Find the best matching file across all open projects
|
||||
*
|
||||
* Search strategy:
|
||||
* 1. Check if query is an absolute path
|
||||
* 2. Search in project source files (exact, path, partial matches)
|
||||
* 3. Search filesystem within project directories (respects .qodeassistignore)
|
||||
*
|
||||
* @param query Filename, partial name, or path to search for (case-insensitive)
|
||||
* @param filePattern Optional file pattern filter (e.g., "*.cpp", "*.h")
|
||||
* @param maxResults Maximum number of candidates to collect
|
||||
* @param ignoreManager IgnoreManager instance for filtering files
|
||||
* @return Best matching file, or empty FileMatch if not found
|
||||
*/
|
||||
static FileMatch findBestMatch(
|
||||
const QString &query,
|
||||
const QString &filePattern = QString(),
|
||||
int maxResults = 10,
|
||||
Context::IgnoreManager *ignoreManager = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Check if a filename matches a file pattern
|
||||
*
|
||||
* Supports:
|
||||
* - Wildcard patterns (*.cpp, *.h)
|
||||
* - Exact filename matching
|
||||
* - Empty pattern (matches all)
|
||||
*
|
||||
* @param fileName File name to check
|
||||
* @param pattern Pattern to match against
|
||||
* @return true if filename matches pattern
|
||||
*/
|
||||
static bool matchesFilePattern(const QString &fileName, const QString &pattern);
|
||||
|
||||
/**
|
||||
* @brief Read file content with security checks
|
||||
*
|
||||
* Performs the following checks:
|
||||
* - File exists and is readable
|
||||
* - Respects project boundary settings (allowAccessOutsideProject)
|
||||
* - Logs access to files outside project scope
|
||||
*
|
||||
* @param filePath Absolute path to file
|
||||
* @return File content as QString, or null QString on error
|
||||
*/
|
||||
static QString readFileContent(const QString &filePath);
|
||||
|
||||
/**
|
||||
* @brief Search for files in filesystem directory tree
|
||||
*
|
||||
* Recursively searches a directory for files matching the query.
|
||||
* Respects .qodeassistignore patterns and depth limits.
|
||||
*
|
||||
* @param dirPath Directory to search in
|
||||
* @param query Search query (case-insensitive)
|
||||
* @param projectName Name of the project for metadata
|
||||
* @param projectDir Root directory of the project
|
||||
* @param project Project instance for ignore checking
|
||||
* @param matches Output list to append matches to
|
||||
* @param maxResults Stop after finding this many matches
|
||||
* @param currentDepth Current recursion depth (modified during recursion)
|
||||
* @param maxDepth Maximum recursion depth
|
||||
* @param ignoreManager IgnoreManager instance for filtering files
|
||||
*/
|
||||
static void searchInFileSystem(
|
||||
const QString &dirPath,
|
||||
const QString &query,
|
||||
const QString &projectName,
|
||||
const QString &projectDir,
|
||||
ProjectExplorer::Project *project,
|
||||
QList<FileMatch> &matches,
|
||||
int maxResults,
|
||||
int ¤tDepth,
|
||||
int maxDepth = 5,
|
||||
Context::IgnoreManager *ignoreManager = nullptr);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
@ -20,17 +20,9 @@
|
||||
#include "FindAndReadFileTool.hpp"
|
||||
#include "ToolExceptions.hpp"
|
||||
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QTextStream>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
@ -109,14 +101,15 @@ QFuture<QString> FindAndReadFileTool::executeAsync(const QJsonObject &input)
|
||||
.arg(query, filePattern.isEmpty() ? "none" : filePattern)
|
||||
.arg(readContent));
|
||||
|
||||
FileMatch bestMatch = findBestMatch(query, filePattern, 10);
|
||||
FileSearchUtils::FileMatch bestMatch = FileSearchUtils::findBestMatch(
|
||||
query, filePattern, 10, m_ignoreManager);
|
||||
|
||||
if (bestMatch.absolutePath.isEmpty()) {
|
||||
return QString("No file found matching '%1'").arg(query);
|
||||
}
|
||||
|
||||
if (readContent) {
|
||||
bestMatch.content = readFileContent(bestMatch.absolutePath);
|
||||
bestMatch.content = FileSearchUtils::readFileContent(bestMatch.absolutePath);
|
||||
if (bestMatch.content.isNull()) {
|
||||
bestMatch.error = "Could not read file";
|
||||
}
|
||||
@ -126,221 +119,8 @@ QFuture<QString> FindAndReadFileTool::executeAsync(const QJsonObject &input)
|
||||
});
|
||||
}
|
||||
|
||||
FindAndReadFileTool::FileMatch FindAndReadFileTool::findBestMatch(
|
||||
const QString &query, const QString &filePattern, int maxResults)
|
||||
{
|
||||
QList<FileMatch> candidates;
|
||||
auto projects = ProjectExplorer::ProjectManager::projects();
|
||||
|
||||
if (projects.isEmpty()) {
|
||||
return FileMatch{};
|
||||
}
|
||||
|
||||
QFileInfo queryInfo(query);
|
||||
if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) {
|
||||
FileMatch match;
|
||||
match.absolutePath = queryInfo.canonicalFilePath();
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
if (match.absolutePath.startsWith(projectDir)) {
|
||||
match.relativePath = QDir(projectDir).relativeFilePath(match.absolutePath);
|
||||
match.projectName = project->displayName();
|
||||
match.matchType = MatchType::ExactName;
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
match.relativePath = queryInfo.fileName();
|
||||
match.projectName = "External";
|
||||
match.matchType = MatchType::ExactName;
|
||||
return match;
|
||||
}
|
||||
|
||||
QString lowerQuery = query.toLower();
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
QString projectName = project->displayName();
|
||||
|
||||
for (const auto &filePath : projectFiles) {
|
||||
QString absolutePath = filePath.path();
|
||||
if (m_ignoreManager->shouldIgnore(absolutePath, project))
|
||||
continue;
|
||||
|
||||
QFileInfo fileInfo(absolutePath);
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
if (!filePattern.isEmpty() && !matchesFilePattern(fileName, filePattern))
|
||||
continue;
|
||||
|
||||
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||
|
||||
FileMatch match;
|
||||
match.absolutePath = absolutePath;
|
||||
match.relativePath = relativePath;
|
||||
match.projectName = projectName;
|
||||
|
||||
QString lowerFileName = fileName.toLower();
|
||||
QString lowerRelativePath = relativePath.toLower();
|
||||
|
||||
if (lowerFileName == lowerQuery) {
|
||||
match.matchType = MatchType::ExactName;
|
||||
candidates.append(match);
|
||||
} else if (lowerRelativePath.contains(lowerQuery)) {
|
||||
match.matchType = MatchType::PathMatch;
|
||||
candidates.append(match);
|
||||
} else if (lowerFileName.contains(lowerQuery)) {
|
||||
match.matchType = MatchType::PartialName;
|
||||
candidates.append(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isEmpty() || candidates.first().matchType != MatchType::ExactName) {
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
QString projectName = project->displayName();
|
||||
int depth = 0;
|
||||
searchInFileSystem(
|
||||
projectDir,
|
||||
lowerQuery,
|
||||
projectName,
|
||||
projectDir,
|
||||
project,
|
||||
candidates,
|
||||
maxResults,
|
||||
depth);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
return FileMatch{};
|
||||
}
|
||||
|
||||
std::sort(candidates.begin(), candidates.end());
|
||||
return candidates.first();
|
||||
}
|
||||
|
||||
void FindAndReadFileTool::searchInFileSystem(
|
||||
const QString &dirPath,
|
||||
const QString &query,
|
||||
const QString &projectName,
|
||||
const QString &projectDir,
|
||||
ProjectExplorer::Project *project,
|
||||
QList<FileMatch> &matches,
|
||||
int maxResults,
|
||||
int ¤tDepth,
|
||||
int maxDepth)
|
||||
{
|
||||
if (currentDepth >= maxDepth || matches.size() >= maxResults)
|
||||
return;
|
||||
|
||||
currentDepth++;
|
||||
QDir dir(dirPath);
|
||||
if (!dir.exists()) {
|
||||
currentDepth--;
|
||||
return;
|
||||
}
|
||||
|
||||
auto entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const auto &entry : entries) {
|
||||
if (matches.size() >= maxResults)
|
||||
break;
|
||||
|
||||
QString absolutePath = entry.absoluteFilePath();
|
||||
if (m_ignoreManager->shouldIgnore(absolutePath, project))
|
||||
continue;
|
||||
|
||||
QString fileName = entry.fileName();
|
||||
|
||||
if (entry.isDir()) {
|
||||
searchInFileSystem(
|
||||
absolutePath,
|
||||
query,
|
||||
projectName,
|
||||
projectDir,
|
||||
project,
|
||||
matches,
|
||||
maxResults,
|
||||
currentDepth,
|
||||
maxDepth);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString lowerFileName = fileName.toLower();
|
||||
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||
QString lowerRelativePath = relativePath.toLower();
|
||||
|
||||
FileMatch match;
|
||||
match.absolutePath = absolutePath;
|
||||
match.relativePath = relativePath;
|
||||
match.projectName = projectName;
|
||||
|
||||
if (lowerFileName == query) {
|
||||
match.matchType = MatchType::ExactName;
|
||||
matches.append(match);
|
||||
} else if (lowerRelativePath.contains(query)) {
|
||||
match.matchType = MatchType::PathMatch;
|
||||
matches.append(match);
|
||||
} else if (lowerFileName.contains(query)) {
|
||||
match.matchType = MatchType::PartialName;
|
||||
matches.append(match);
|
||||
}
|
||||
}
|
||||
|
||||
currentDepth--;
|
||||
}
|
||||
|
||||
bool FindAndReadFileTool::matchesFilePattern(const QString &fileName, const QString &pattern) const
|
||||
{
|
||||
if (pattern.isEmpty())
|
||||
return true;
|
||||
|
||||
if (pattern.startsWith("*.")) {
|
||||
QString extension = pattern.mid(1);
|
||||
return fileName.endsWith(extension, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return fileName.compare(pattern, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
QString FindAndReadFileTool::readFileContent(const QString &filePath) const
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString canonicalPath = QFileInfo(filePath).canonicalFilePath();
|
||||
bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath);
|
||||
|
||||
if (!isInProject) {
|
||||
const auto &settings = Settings::generalSettings();
|
||||
if (!settings.allowAccessOutsideProject()) {
|
||||
LOG_MESSAGE(QString("Access denied to file outside project: %1").arg(canonicalPath));
|
||||
return QString();
|
||||
}
|
||||
LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath));
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setAutoDetectUnicode(true);
|
||||
QString content = stream.readAll();
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
QString FindAndReadFileTool::formatResult(const FileMatch &match, bool readContent) const
|
||||
QString FindAndReadFileTool::formatResult(const FileSearchUtils::FileMatch &match,
|
||||
bool readContent) const
|
||||
{
|
||||
QString result
|
||||
= QString("Found file: %1\nAbsolute path: %2").arg(match.relativePath, match.absolutePath);
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FileSearchUtils.hpp"
|
||||
|
||||
#include <context/IgnoreManager.hpp>
|
||||
#include <llmcore/BaseTool.hpp>
|
||||
#include <QFuture>
|
||||
@ -42,38 +44,7 @@ public:
|
||||
QFuture<QString> executeAsync(const QJsonObject &input) override;
|
||||
|
||||
private:
|
||||
enum class MatchType { ExactName, PathMatch, PartialName };
|
||||
|
||||
struct FileMatch
|
||||
{
|
||||
QString absolutePath;
|
||||
QString relativePath;
|
||||
QString projectName;
|
||||
QString content;
|
||||
MatchType matchType;
|
||||
bool contentRead = false;
|
||||
QString error;
|
||||
|
||||
bool operator<(const FileMatch &other) const
|
||||
{
|
||||
return static_cast<int>(matchType) < static_cast<int>(other.matchType);
|
||||
}
|
||||
};
|
||||
|
||||
FileMatch findBestMatch(const QString &query, const QString &filePattern, int maxResults);
|
||||
void searchInFileSystem(
|
||||
const QString &dirPath,
|
||||
const QString &query,
|
||||
const QString &projectName,
|
||||
const QString &projectDir,
|
||||
ProjectExplorer::Project *project,
|
||||
QList<FileMatch> &matches,
|
||||
int maxResults,
|
||||
int ¤tDepth,
|
||||
int maxDepth = 5);
|
||||
bool matchesFilePattern(const QString &fileName, const QString &pattern) const;
|
||||
QString readFileContent(const QString &filePath) const;
|
||||
QString formatResult(const FileMatch &match, bool readContent) const;
|
||||
QString formatResult(const FileSearchUtils::FileMatch &match, bool readContent) const;
|
||||
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
@ -21,11 +21,13 @@
|
||||
|
||||
#include "logger/Logger.hpp"
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "BuildProjectTool.hpp"
|
||||
#include "CreateNewFileTool.hpp"
|
||||
#include "EditFileTool.hpp"
|
||||
#include "FindAndReadFileTool.hpp"
|
||||
#include "GetIssuesListTool.hpp"
|
||||
#include "ListProjectFilesTool.hpp"
|
||||
@ -46,6 +48,7 @@ void ToolsFactory::registerTools()
|
||||
registerTool(new ListProjectFilesTool(this));
|
||||
registerTool(new GetIssuesListTool(this));
|
||||
registerTool(new CreateNewFileTool(this));
|
||||
registerTool(new EditFileTool(this));
|
||||
registerTool(new BuildProjectTool(this));
|
||||
registerTool(new ProjectSearchTool(this));
|
||||
registerTool(new FindAndReadFileTool(this));
|
||||
@ -81,13 +84,17 @@ LLMCore::BaseTool *ToolsFactory::getToolByName(const QString &name) const
|
||||
QJsonArray ToolsFactory::getToolsDefinitions(LLMCore::ToolSchemaFormat format) const
|
||||
{
|
||||
QJsonArray toolsArray;
|
||||
const auto &settings = Settings::generalSettings();
|
||||
const auto &settings = Settings::toolsSettings();
|
||||
|
||||
for (auto it = m_tools.constBegin(); it != m_tools.constEnd(); ++it) {
|
||||
if (!it.value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (it.value()->name() == "edit_file" && !settings.enableEditFileTool()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto requiredPerms = it.value()->requiredPermissions();
|
||||
bool hasPermission = true;
|
||||
|
||||
|
||||
@ -59,7 +59,6 @@ void ToolsManager::executeToolCall(
|
||||
|
||||
auto &queue = m_toolQueues[requestId];
|
||||
|
||||
// Check if tool already exists in queue or completed
|
||||
for (const auto &tool : queue.queue) {
|
||||
if (tool.id == toolId) {
|
||||
LOG_MESSAGE(QString("Tool %1 already in queue for request %2").arg(toolId, requestId));
|
||||
@ -73,15 +72,16 @@ void ToolsManager::executeToolCall(
|
||||
return;
|
||||
}
|
||||
|
||||
// Add tool to queue
|
||||
PendingTool pendingTool{toolId, toolName, input, "", false};
|
||||
QJsonObject modifiedInput = input;
|
||||
modifiedInput["_request_id"] = requestId;
|
||||
|
||||
PendingTool pendingTool{toolId, toolName, modifiedInput, "", false};
|
||||
queue.queue.append(pendingTool);
|
||||
|
||||
LOG_MESSAGE(QString("ToolsManager: Tool %1 added to queue (position %2)")
|
||||
.arg(toolName)
|
||||
.arg(queue.queue.size()));
|
||||
|
||||
// Start execution if not already running
|
||||
if (!queue.isExecuting) {
|
||||
executeNextTool(requestId);
|
||||
}
|
||||
@ -95,7 +95,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
|
||||
|
||||
auto &queue = m_toolQueues[requestId];
|
||||
|
||||
// Check if queue is empty
|
||||
if (queue.queue.isEmpty()) {
|
||||
LOG_MESSAGE(QString("ToolsManager: All tools complete for request %1, emitting results")
|
||||
.arg(requestId));
|
||||
@ -105,7 +104,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
|
||||
return;
|
||||
}
|
||||
|
||||
// Get next tool from queue
|
||||
PendingTool tool = queue.queue.takeFirst();
|
||||
queue.isExecuting = true;
|
||||
|
||||
@ -116,7 +114,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
|
||||
auto toolInstance = m_toolsFactory->getToolByName(tool.name);
|
||||
if (!toolInstance) {
|
||||
LOG_MESSAGE(QString("ToolsManager: Tool not found: %1").arg(tool.name));
|
||||
// Mark as failed and continue to next tool
|
||||
tool.result = QString("Error: Tool not found: %1").arg(tool.name);
|
||||
tool.complete = true;
|
||||
queue.completed[tool.id] = tool;
|
||||
@ -124,7 +121,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
|
||||
return;
|
||||
}
|
||||
|
||||
// Store tool in completed map (will be updated when finished)
|
||||
queue.completed[tool.id] = tool;
|
||||
|
||||
m_toolHandler->executeToolAsync(requestId, tool.id, toolInstance, tool.input);
|
||||
@ -176,7 +172,6 @@ void ToolsManager::onToolFinished(
|
||||
.arg(success ? QString("completed") : QString("failed"))
|
||||
.arg(requestId));
|
||||
|
||||
// Execute next tool in queue
|
||||
executeNextTool(requestId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user