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()));
}