mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-11-12 21:12:44 -05:00
feat: Add find file tool
This commit is contained in:
@ -124,13 +124,13 @@ add_qtc_plugin(QodeAssist
|
||||
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
|
||||
tools/EditProjectFileTool.hpp tools/EditProjectFileTool.cpp
|
||||
tools/FindSymbolTool.hpp tools/FindSymbolTool.cpp
|
||||
tools/FindFileTool.hpp tools/FindFileTool.cpp
|
||||
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
||||
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
||||
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
||||
providers/GoogleMessage.hpp providers/GoogleMessage.cpp
|
||||
)
|
||||
|
||||
|
||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||
find_program(QtCreatorExecutable
|
||||
NAMES
|
||||
|
||||
332
tools/FindFileTool.cpp
Normal file
332
tools/FindFileTool.cpp
Normal file
@ -0,0 +1,332 @@
|
||||
/*
|
||||
* 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 "FindFileTool.hpp"
|
||||
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
FindFileTool::FindFileTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
, m_ignoreManager(new Context::IgnoreManager(this))
|
||||
{}
|
||||
|
||||
QString FindFileTool::name() const
|
||||
{
|
||||
return "find_file";
|
||||
}
|
||||
|
||||
QString FindFileTool::stringName() const
|
||||
{
|
||||
return {"Finding file in project"};
|
||||
}
|
||||
|
||||
QString FindFileTool::description() const
|
||||
{
|
||||
return "Search for files in the current project by filename, partial filename, or path "
|
||||
"(relative or absolute). "
|
||||
"This tool searches for files within the project scope and supports:\n"
|
||||
"- Exact filename match (e.g., 'main.cpp')\n"
|
||||
"- Partial filename match (e.g., 'main' will find 'main.cpp', 'main.h', etc.)\n"
|
||||
"- Relative path from project root (e.g., 'src/utils/helper.cpp')\n"
|
||||
"- Partial path matching (e.g., 'utils/helper' will find matching paths)\n"
|
||||
"- File extension filtering (e.g., '*.cpp', '*.h')\n"
|
||||
"- Case-insensitive search\n"
|
||||
"Input parameters:\n"
|
||||
"- 'query' (required): the filename, partial name, or path to search for\n"
|
||||
"- 'file_pattern' (optional): filter by file extension (e.g., '*.cpp', '*.h')\n"
|
||||
"- 'max_results' (optional): maximum number of results to return (default: 50)\n"
|
||||
"Returns a list of matching files with their absolute paths and relative paths from "
|
||||
"project root, "
|
||||
"or an error if no files are found or if the file is outside the project scope.";
|
||||
}
|
||||
|
||||
QJsonObject FindFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
QJsonObject queryProperty;
|
||||
queryProperty["type"] = "string";
|
||||
queryProperty["description"]
|
||||
= "The filename, partial filename, or path to search for (case-insensitive)";
|
||||
properties["query"] = queryProperty;
|
||||
|
||||
QJsonObject filePatternProperty;
|
||||
filePatternProperty["type"] = "string";
|
||||
filePatternProperty["description"]
|
||||
= "Optional file pattern to filter results (e.g., '*.cpp', '*.h', '*.qml')";
|
||||
properties["file_pattern"] = filePatternProperty;
|
||||
|
||||
QJsonObject maxResultsProperty;
|
||||
maxResultsProperty["type"] = "integer";
|
||||
maxResultsProperty["description"]
|
||||
= "Maximum number of results to return (default: 50, max: 200)";
|
||||
maxResultsProperty["default"] = DEFAULT_MAX_RESULTS;
|
||||
properties["max_results"] = maxResultsProperty;
|
||||
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = properties;
|
||||
|
||||
QJsonArray required;
|
||||
required.append("query");
|
||||
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 FindFileTool::requiredPermissions() const
|
||||
{
|
||||
return LLMCore::ToolPermission::FileSystemRead;
|
||||
}
|
||||
|
||||
QFuture<QString> FindFileTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> QString {
|
||||
QString query = input["query"].toString().trimmed();
|
||||
if (query.isEmpty()) {
|
||||
QString error = "Error: query parameter is required and cannot be empty";
|
||||
throw std::invalid_argument(error.toStdString());
|
||||
}
|
||||
|
||||
QString filePattern = input["file_pattern"].toString().trimmed();
|
||||
int maxResults = input["max_results"].toInt(DEFAULT_MAX_RESULTS);
|
||||
|
||||
maxResults = qBound(1, maxResults, 200);
|
||||
|
||||
LOG_MESSAGE(QString("FindFileTool: Searching for '%1'%2 (max: %3)")
|
||||
.arg(query)
|
||||
.arg(filePattern.isEmpty() ? "" : QString(" with pattern '%1'").arg(filePattern))
|
||||
.arg(maxResults));
|
||||
|
||||
QFileInfo queryInfo(query);
|
||||
if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) {
|
||||
QString canonicalPath = queryInfo.canonicalFilePath();
|
||||
if (!isFileInProject(canonicalPath)) {
|
||||
QString error = QString("Error: File '%1' exists but is outside the project scope. "
|
||||
"Only files within the project can be accessed.")
|
||||
.arg(canonicalPath);
|
||||
throw std::runtime_error(error.toStdString());
|
||||
}
|
||||
|
||||
auto project = ProjectExplorer::ProjectManager::projectForFile(
|
||||
Utils::FilePath::fromString(canonicalPath));
|
||||
|
||||
if (project && !m_ignoreManager->shouldIgnore(canonicalPath, project)) {
|
||||
FileMatch match;
|
||||
match.absolutePath = canonicalPath;
|
||||
match.relativePath = QDir(project->projectDirectory().toFSPathString())
|
||||
.relativeFilePath(canonicalPath);
|
||||
match.projectName = project->displayName();
|
||||
match.matchType = FileMatch::PathMatch;
|
||||
|
||||
QList<FileMatch> matches;
|
||||
matches.append(match);
|
||||
return formatResults(matches, 1, maxResults);
|
||||
}
|
||||
}
|
||||
|
||||
QList<FileMatch> matches = findMatchingFiles(query, maxResults);
|
||||
int totalFound = matches.size();
|
||||
|
||||
if (matches.isEmpty()) {
|
||||
QString error = QString("Error: No files found matching '%1'%2 in the project. "
|
||||
"Try using a different search term or check the file name.")
|
||||
.arg(query)
|
||||
.arg(filePattern.isEmpty() ? "" : QString(" with pattern '%1'").arg(filePattern));
|
||||
throw std::runtime_error(error.toStdString());
|
||||
}
|
||||
|
||||
return formatResults(matches, totalFound, maxResults);
|
||||
});
|
||||
}
|
||||
|
||||
QList<FindFileTool::FileMatch> FindFileTool::findMatchingFiles(const QString &query,
|
||||
int maxResults) const
|
||||
{
|
||||
QList<FileMatch> matches;
|
||||
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
|
||||
|
||||
if (projects.isEmpty()) {
|
||||
LOG_MESSAGE("FindFileTool: No projects are currently open");
|
||||
return matches;
|
||||
}
|
||||
|
||||
QString lowerQuery = query.toLower();
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
Utils::FilePath projectDir = project->projectDirectory();
|
||||
QString projectName = project->displayName();
|
||||
|
||||
for (const auto &filePath : projectFiles) {
|
||||
if (matches.size() >= maxResults) {
|
||||
break;
|
||||
}
|
||||
|
||||
QString absolutePath = filePath.toFSPathString();
|
||||
|
||||
if (m_ignoreManager->shouldIgnore(absolutePath, project)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(absolutePath);
|
||||
QString fileName = fileInfo.fileName();
|
||||
QString relativePath = QDir(projectDir.toFSPathString()).relativeFilePath(absolutePath);
|
||||
|
||||
FileMatch match;
|
||||
match.absolutePath = absolutePath;
|
||||
match.relativePath = relativePath;
|
||||
match.projectName = projectName;
|
||||
|
||||
if (fileName.toLower() == lowerQuery) {
|
||||
match.matchType = FileMatch::ExactName;
|
||||
matches.append(match);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (relativePath.toLower().contains(lowerQuery)) {
|
||||
match.matchType = FileMatch::PathMatch;
|
||||
matches.append(match);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fileName.toLower().contains(lowerQuery)) {
|
||||
match.matchType = FileMatch::PartialName;
|
||||
matches.append(match);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.size() >= maxResults) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(matches.begin(), matches.end());
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
bool FindFileTool::matchesFilePattern(const QString &fileName, const QString &pattern) const
|
||||
{
|
||||
if (pattern.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pattern.startsWith("*.")) {
|
||||
QString extension = pattern.mid(1); // Remove the '*'
|
||||
return fileName.endsWith(extension, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return fileName.compare(pattern, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
QString FindFileTool::formatResults(const QList<FileMatch> &matches,
|
||||
int totalFound,
|
||||
int maxResults) const
|
||||
{
|
||||
QString result;
|
||||
bool wasTruncated = totalFound > matches.size();
|
||||
|
||||
if (matches.size() == 1) {
|
||||
const FileMatch &match = matches.first();
|
||||
result = QString("Found 1 file:\n\n");
|
||||
result += QString("File: %1\n").arg(match.relativePath);
|
||||
result += QString("Absolute path: %2\n").arg(match.absolutePath);
|
||||
result += QString("Project: %3").arg(match.projectName);
|
||||
} else {
|
||||
result = QString("Found %1 file%2%3:\n\n")
|
||||
.arg(totalFound)
|
||||
.arg(totalFound == 1 ? "" : "s")
|
||||
.arg(wasTruncated ? QString(" (showing first %1)").arg(matches.size()) : "");
|
||||
|
||||
QString currentProject;
|
||||
for (const FileMatch &match : matches) {
|
||||
if (currentProject != match.projectName) {
|
||||
if (!currentProject.isEmpty()) {
|
||||
result += "\n";
|
||||
}
|
||||
result += QString("Project '%1':\n").arg(match.projectName);
|
||||
currentProject = match.projectName;
|
||||
}
|
||||
|
||||
result += QString(" - %1\n").arg(match.relativePath);
|
||||
result += QString(" Absolute path: %2\n").arg(match.absolutePath);
|
||||
}
|
||||
|
||||
if (wasTruncated) {
|
||||
result += QString("\n(Note: %1 additional file%2 not shown. "
|
||||
"Use 'max_results' parameter to see more.)")
|
||||
.arg(totalFound - matches.size())
|
||||
.arg(totalFound - matches.size() == 1 ? "" : "s");
|
||||
}
|
||||
}
|
||||
|
||||
return result.trimmed();
|
||||
}
|
||||
|
||||
bool FindFileTool::isFileInProject(const QString &filePath) const
|
||||
{
|
||||
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
|
||||
Utils::FilePath targetPath = Utils::FilePath::fromString(filePath);
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
for (const auto &projectFile : std::as_const(projectFiles)) {
|
||||
if (projectFile == targetPath) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Utils::FilePath projectDir = project->projectDirectory();
|
||||
if (targetPath.isChildOf(projectDir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
67
tools/FindFileTool.hpp
Normal file
67
tools/FindFileTool.hpp
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 <llmcore/BaseTool.hpp>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class FindFileTool : public LLMCore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FindFileTool(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:
|
||||
struct FileMatch
|
||||
{
|
||||
QString absolutePath;
|
||||
QString relativePath;
|
||||
QString projectName;
|
||||
enum MatchType { ExactName, PartialName, PathMatch } matchType;
|
||||
|
||||
bool operator<(const FileMatch &other) const
|
||||
{
|
||||
if (matchType != other.matchType) {
|
||||
return matchType < other.matchType;
|
||||
}
|
||||
return relativePath < other.relativePath;
|
||||
}
|
||||
};
|
||||
|
||||
QList<FileMatch> findMatchingFiles(const QString &query, int maxResults) const;
|
||||
QString formatResults(const QList<FileMatch> &matches, int totalFound, int maxResults) const;
|
||||
bool isFileInProject(const QString &filePath) const;
|
||||
bool matchesFilePattern(const QString &fileName, const QString &pattern) const;
|
||||
|
||||
static constexpr int DEFAULT_MAX_RESULTS = 50;
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
@ -25,6 +25,7 @@
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "EditProjectFileTool.hpp"
|
||||
#include "FindFileTool.hpp"
|
||||
#include "FindSymbolTool.hpp"
|
||||
#include "GetIssuesListTool.hpp"
|
||||
#include "ListProjectFilesTool.hpp"
|
||||
@ -43,12 +44,13 @@ ToolsFactory::ToolsFactory(QObject *parent)
|
||||
void ToolsFactory::registerTools()
|
||||
{
|
||||
registerTool(new ReadVisibleFilesTool(this));
|
||||
registerTool(new ReadProjectFileByNameTool(this));
|
||||
registerTool(new ReadProjectFileByPathTool(this));
|
||||
registerTool(new ListProjectFilesTool(this));
|
||||
registerTool(new SearchInProjectTool(this));
|
||||
registerTool(new GetIssuesListTool(this));
|
||||
registerTool(new EditProjectFileTool(this));
|
||||
registerTool(new FindSymbolTool(this));
|
||||
registerTool(new FindFileTool(this));
|
||||
|
||||
LOG_MESSAGE(QString("Registered %1 tools").arg(m_tools.size()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user