From 56354e8d875692bbd9ef9857e9588d2d65372731 Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:38:12 +0200 Subject: [PATCH] feat: Add find file tool --- CMakeLists.txt | 2 +- tools/FindFileTool.cpp | 332 +++++++++++++++++++++++++++++++++++++++++ tools/FindFileTool.hpp | 67 +++++++++ tools/ToolsFactory.cpp | 4 +- 4 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 tools/FindFileTool.cpp create mode 100644 tools/FindFileTool.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4903605..f4c03c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/tools/FindFileTool.cpp b/tools/FindFileTool.cpp new file mode 100644 index 0000000..137b086 --- /dev/null +++ b/tools/FindFileTool.cpp @@ -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 . + */ + +#include "FindFileTool.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 matches; + matches.append(match); + return formatResults(matches, 1, maxResults); + } + } + + QList 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::findMatchingFiles(const QString &query, + int maxResults) const +{ + QList matches; + QList 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 &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 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 diff --git a/tools/FindFileTool.hpp b/tools/FindFileTool.hpp new file mode 100644 index 0000000..db6af26 --- /dev/null +++ b/tools/FindFileTool.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +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 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 findMatchingFiles(const QString &query, int maxResults) const; + QString formatResults(const QList &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 diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp index aab3387..3ae73b7 100644 --- a/tools/ToolsFactory.cpp +++ b/tools/ToolsFactory.cpp @@ -25,6 +25,7 @@ #include #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())); }