refactor: Optimize searching tools

refactor: Merge read and find tool
This commit is contained in:
Petr Mironychev
2025-10-29 00:19:49 +01:00
parent 3b56c1f07a
commit 5a49a2e7eb
12 changed files with 772 additions and 1648 deletions

View File

@ -115,18 +115,15 @@ add_qtc_plugin(QodeAssist
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
tools/ReadFilesByPathTool.hpp tools/ReadFilesByPathTool.cpp
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
tools/ToolHandler.hpp tools/ToolHandler.cpp
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
tools/ToolsManager.hpp tools/ToolsManager.cpp
tools/SearchInProjectTool.hpp tools/SearchInProjectTool.cpp
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
tools/FindSymbolTool.hpp tools/FindSymbolTool.cpp
tools/FindFileTool.hpp tools/FindFileTool.cpp
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
providers/OllamaMessage.hpp providers/OllamaMessage.cpp

View File

@ -0,0 +1,359 @@
/*
* 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 "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 {
FindAndReadFileTool::FindAndReadFileTool(QObject *parent)
: BaseTool(parent)
, m_ignoreManager(new Context::IgnoreManager(this))
{}
QString FindAndReadFileTool::name() const
{
return "find_and_read_file";
}
QString FindAndReadFileTool::stringName() const
{
return "Finding and reading file";
}
QString FindAndReadFileTool::description() const
{
return "Search for a file by name/path and optionally read its content. "
"Returns the best matching file and its content.";
}
QJsonObject FindAndReadFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
{
QJsonObject properties;
properties["query"] = QJsonObject{
{"type", "string"},
{"description", "Filename, partial name, or path to search for (case-insensitive)"}};
properties["file_pattern"] = QJsonObject{
{"type", "string"}, {"description", "File pattern filter (e.g., '*.cpp', '*.h', '*.qml')"}};
properties["read_content"] = QJsonObject{
{"type", "boolean"},
{"description", "Read file content in addition to finding path (default: true)"}};
QJsonObject definition;
definition["type"] = "object";
definition["properties"] = properties;
definition["required"] = QJsonArray{"query"};
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 FindAndReadFileTool::requiredPermissions() const
{
return LLMCore::ToolPermission::FileSystemRead;
}
QFuture<QString> FindAndReadFileTool::executeAsync(const QJsonObject &input)
{
return QtConcurrent::run([this, input]() -> QString {
QString query = input["query"].toString().trimmed();
if (query.isEmpty()) {
throw ToolInvalidArgument("Query parameter is required");
}
QString filePattern = input["file_pattern"].toString();
bool readContent = input["read_content"].toBool(true);
LOG_MESSAGE(QString("FindAndReadFileTool: Searching for '%1' (pattern: %2, read: %3)")
.arg(query, filePattern.isEmpty() ? "none" : filePattern)
.arg(readContent));
FileMatch bestMatch = findBestMatch(query, filePattern, 10);
if (bestMatch.absolutePath.isEmpty()) {
return QString("No file found matching '%1'").arg(query);
}
if (readContent) {
bestMatch.content = readFileContent(bestMatch.absolutePath);
if (bestMatch.content.isNull()) {
bestMatch.error = "Could not read file";
}
}
return formatResult(bestMatch, readContent);
});
}
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 &currentDepth,
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 result
= QString("Found file: %1\nAbsolute path: %2").arg(match.relativePath, match.absolutePath);
if (readContent) {
if (!match.error.isEmpty()) {
result += QString("\nError: %1").arg(match.error);
} else {
result += QString("\n\n=== Content ===\n%1").arg(match.content);
}
}
return result;
}
} // namespace QodeAssist::Tools

View File

@ -21,42 +21,48 @@
#include <context/IgnoreManager.hpp>
#include <llmcore/BaseTool.hpp>
#include <QFuture>
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::Tools {
class FindFileTool : public LLMCore::BaseTool
class FindAndReadFileTool : public LLMCore::BaseTool
{
Q_OBJECT
public:
explicit FindFileTool(QObject *parent = nullptr);
explicit FindAndReadFileTool(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;
QFuture<QString> executeAsync(const QJsonObject &input) override;
private:
enum class MatchType { ExactName, PathMatch, PartialName };
struct FileMatch
{
QString absolutePath;
QString relativePath;
QString projectName;
enum MatchType { ExactName, PartialName, PathMatch } matchType;
QString content;
MatchType matchType;
bool contentRead = false;
QString error;
bool operator<(const FileMatch &other) const
{
if (matchType != other.matchType) {
return matchType < other.matchType;
}
return relativePath < other.relativePath;
return static_cast<int>(matchType) < static_cast<int>(other.matchType);
}
};
QList<FileMatch> findMatchingFiles(const QString &query, int maxResults) const;
void searchInFileSystem(const QString &dirPath,
FileMatch findBestMatch(const QString &query, const QString &filePattern, int maxResults);
void searchInFileSystem(
const QString &dirPath,
const QString &query,
const QString &projectName,
const QString &projectDir,
@ -64,11 +70,11 @@ private:
QList<FileMatch> &matches,
int maxResults,
int &currentDepth,
int maxDepth = 10) const;
QString formatResults(const QList<FileMatch> &matches, int totalFound, int maxResults) const;
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;
static constexpr int DEFAULT_MAX_RESULTS = 50;
Context::IgnoreManager *m_ignoreManager;
};

View File

@ -1,430 +0,0 @@
/*
* 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 "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 <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 project by filename, partial name, or path. "
"Searches both in CMake-registered files and filesystem (finds .gitignore, Python scripts, README, etc.). "
"Supports exact/partial filename match, relative/absolute paths, file extension filtering, "
"and case-insensitive search. "
"Returns matching files with absolute and relative paths.";
}
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). "
"Finds ALL files in project directory including .gitignore, README.md, Python scripts, "
"config files, etc., even if not in CMake build system";
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 ToolInvalidArgument(error);
}
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("")
: QString(" with pattern '%1'").arg(filePattern))
.arg(maxResults));
QFileInfo queryInfo(query);
if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) {
QString canonicalPath = queryInfo.canonicalFilePath();
bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath);
// Check if reading outside project is allowed
if (!isInProject) {
const auto &settings = Settings::generalSettings();
if (!settings.allowAccessOutsideProject()) {
QString error = QString("Error: File '%1' exists but is outside the project scope. "
"Enable 'Allow file access outside project' in settings to access files outside project scope.")
.arg(canonicalPath);
throw std::runtime_error(error.toStdString());
}
LOG_MESSAGE(QString("Finding file outside project scope: %1").arg(canonicalPath));
}
auto project = isInProject ? ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(canonicalPath)) : nullptr;
if (!isInProject || (project && !m_ignoreManager->shouldIgnore(canonicalPath, project))) {
FileMatch match;
match.absolutePath = canonicalPath;
match.relativePath = isInProject && project
? QDir(project->projectDirectory().toFSPathString()).relativeFilePath(canonicalPath)
: canonicalPath;
match.projectName = isInProject && project ? project->displayName() : "External";
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("")
: 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;
}
}
// If we didn't find enough matches in project files, search the filesystem
if (matches.size() < maxResults) {
LOG_MESSAGE(QString("FindFileTool: Extending search to filesystem (found %1 matches so far)")
.arg(matches.size()));
for (auto project : projects) {
if (!project)
continue;
if (matches.size() >= maxResults) {
break;
}
Utils::FilePath projectDir = project->projectDirectory();
QString projectName = project->displayName();
QString projectDirStr = projectDir.toFSPathString();
int depth = 0;
searchInFileSystem(projectDirStr, lowerQuery, projectName, projectDirStr,
project, matches, maxResults, depth);
}
}
std::sort(matches.begin(), matches.end());
return matches;
}
void FindFileTool::searchInFileSystem(const QString &dirPath,
const QString &query,
const QString &projectName,
const QString &projectDir,
ProjectExplorer::Project *project,
QList<FileMatch> &matches,
int maxResults,
int &currentDepth,
int maxDepth) const
{
if (currentDepth > maxDepth || matches.size() >= maxResults) {
return;
}
currentDepth++;
QDir dir(dirPath);
if (!dir.exists()) {
currentDepth--;
return;
}
// Get all entries (files and directories)
QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden);
for (const QFileInfo &entry : entries) {
if (matches.size() >= maxResults) {
break;
}
QString absolutePath = entry.absoluteFilePath();
// Check if should be ignored
if (project && m_ignoreManager->shouldIgnore(absolutePath, project)) {
continue;
}
// Skip common build/cache directories
QString fileName = entry.fileName();
if (entry.isDir()) {
// Skip common build/cache directories
if (fileName == "build" || fileName == ".git" || fileName == "node_modules" ||
fileName == "__pycache__" || fileName == ".venv" || fileName == "venv" ||
fileName == ".cmake" || fileName == "CMakeFiles" || fileName.startsWith(".qt")) {
continue;
}
// Recurse into subdirectory
searchInFileSystem(absolutePath, query, projectName, projectDir,
project, matches, maxResults, currentDepth, maxDepth);
continue;
}
// Check if already in matches (avoid duplicates from project files)
bool alreadyAdded = false;
for (const auto &match : matches) {
if (match.absolutePath == absolutePath) {
alreadyAdded = true;
break;
}
}
if (alreadyAdded) {
continue;
}
// Match logic
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 = FileMatch::ExactName;
matches.append(match);
} else if (lowerRelativePath.contains(query)) {
match.matchType = FileMatch::PathMatch;
matches.append(match);
} else if (lowerFileName.contains(query)) {
match.matchType = FileMatch::PartialName;
matches.append(match);
}
}
currentDepth--;
}
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 ? QString("") : QString("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 ? QString("") : QString("s"));
}
}
return result.trimmed();
}
} // namespace QodeAssist::Tools

View File

@ -1,441 +0,0 @@
/*
* 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 "FindSymbolTool.hpp"
#include "ToolExceptions.hpp"
#include <cplusplus/Overview.h>
#include <cplusplus/Scope.h>
#include <cplusplus/Symbols.h>
#include <cppeditor/cppmodelmanager.h>
#include <logger/Logger.hpp>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <QJsonArray>
#include <QJsonObject>
#include <QtConcurrent>
namespace QodeAssist::Tools {
FindSymbolTool::FindSymbolTool(QObject *parent)
: BaseTool(parent)
, m_ignoreManager(new Context::IgnoreManager(this))
{}
QString FindSymbolTool::name() const
{
return "find_cpp_symbol";
}
QString FindSymbolTool::stringName() const
{
return "Finding C++ symbols in project";
}
QString FindSymbolTool::description() const
{
return "Find C++ symbols (classes, functions, enums, variables, typedefs, namespaces) in the project. "
"Returns file paths and line numbers. "
"Supports exact match, wildcards (* patterns), and regex. "
"Use read_files_by_path to read the actual code after finding symbols.";
}
QJsonObject FindSymbolTool::getDefinition(LLMCore::ToolSchemaFormat format) const
{
QJsonObject properties;
QJsonObject symbolNameProperty;
symbolNameProperty["type"] = "string";
symbolNameProperty["description"] = "Name or pattern of the symbol to find (supports exact "
"match, wildcard, or regex depending on flags)";
properties["symbol_name"] = symbolNameProperty;
QJsonObject symbolTypeProperty;
symbolTypeProperty["type"] = "string";
symbolTypeProperty["description"]
= "Type of symbol: all, class, function, enum, variable, typedef, namespace";
symbolTypeProperty["enum"]
= QJsonArray{"all", "class", "function", "enum", "variable", "typedef", "namespace"};
properties["symbol_type"] = symbolTypeProperty;
QJsonObject scopeFilterProperty;
scopeFilterProperty["type"] = "string";
scopeFilterProperty["description"] = "Filter results by scope (e.g., 'MyNamespace', 'MyClass')";
properties["scope_filter"] = scopeFilterProperty;
QJsonObject caseSensitiveProperty;
caseSensitiveProperty["type"] = "boolean";
caseSensitiveProperty["description"] = "Enable case-sensitive search (default: true)";
properties["case_sensitive"] = caseSensitiveProperty;
QJsonObject useRegexProperty;
useRegexProperty["type"] = "boolean";
useRegexProperty["description"] = "Treat symbol_name as regular expression (default: false)";
properties["use_regex"] = useRegexProperty;
QJsonObject useWildcardProperty;
useWildcardProperty["type"] = "boolean";
useWildcardProperty["description"]
= "Treat symbol_name as wildcard pattern like 'find*', '*Symbol' (default: false)";
properties["use_wildcard"] = useWildcardProperty;
QJsonObject maxResultsProperty;
maxResultsProperty["type"] = "integer";
maxResultsProperty["description"] = "Maximum number of results to return (default: 50)";
maxResultsProperty["default"] = 50;
properties["max_results"] = maxResultsProperty;
QJsonObject definition;
definition["type"] = "object";
definition["properties"] = properties;
definition["required"] = QJsonArray{"symbol_name"};
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 FindSymbolTool::requiredPermissions() const
{
return LLMCore::ToolPermission::FileSystemRead;
}
QFuture<QString> FindSymbolTool::executeAsync(const QJsonObject &input)
{
return QtConcurrent::run([this, input]() -> QString {
QString symbolName = input["symbol_name"].toString();
QString symbolTypeStr = input["symbol_type"].toString("all");
QString scopeFilter = input["scope_filter"].toString();
bool caseSensitive = input["case_sensitive"].toBool(true);
bool useRegex = input["use_regex"].toBool(false);
bool useWildcard = input["use_wildcard"].toBool(false);
int maxResults = input["max_results"].toInt(50);
if (symbolName.isEmpty()) {
QString error = "Error: 'symbol_name' parameter is required";
LOG_MESSAGE(error);
throw ToolInvalidArgument(error);
}
if (useRegex && useWildcard) {
QString error = "Error: 'use_regex' and 'use_wildcard' cannot be used together";
LOG_MESSAGE(error);
throw ToolInvalidArgument(error);
}
SymbolType type = parseSymbolType(symbolTypeStr);
LOG_MESSAGE(QString(
"Searching for symbol: '%1', type: %2, scope: '%3', "
"case_sensitive: %4, regex: %5, wildcard: %6")
.arg(symbolName, symbolTypeStr, scopeFilter)
.arg(caseSensitive)
.arg(useRegex)
.arg(useWildcard));
QList<SymbolInfo> symbols
= findSymbols(symbolName, type, scopeFilter, caseSensitive, useRegex, useWildcard);
if (symbols.isEmpty()) {
QString msg = QString("No symbol matching '%1' found in the project").arg(symbolName);
if (!scopeFilter.isEmpty()) {
msg += QString(" within scope '%1'").arg(scopeFilter);
}
return msg;
}
if (symbols.size() > maxResults) {
symbols = symbols.mid(0, maxResults);
}
return formatResults(symbols);
});
}
FindSymbolTool::SymbolType FindSymbolTool::parseSymbolType(const QString &typeStr) const
{
if (typeStr == "class")
return SymbolType::Class;
if (typeStr == "function")
return SymbolType::Function;
if (typeStr == "enum")
return SymbolType::Enum;
if (typeStr == "variable")
return SymbolType::Variable;
if (typeStr == "typedef")
return SymbolType::Typedef;
if (typeStr == "namespace")
return SymbolType::Namespace;
return SymbolType::All;
}
QList<FindSymbolTool::SymbolInfo> FindSymbolTool::findSymbols(
const QString &symbolName,
SymbolType type,
const QString &scopeFilter,
bool caseSensitive,
bool useRegex,
bool useWildcard) const
{
QList<SymbolInfo> results;
auto *modelManager = CppEditor::CppModelManager::instance();
if (!modelManager) {
LOG_MESSAGE("CppModelManager not available");
return results;
}
QRegularExpression searchPattern;
if (useRegex) {
QRegularExpression::PatternOptions options = QRegularExpression::NoPatternOption;
if (!caseSensitive) {
options |= QRegularExpression::CaseInsensitiveOption;
}
searchPattern.setPattern(symbolName);
searchPattern.setPatternOptions(options);
if (!searchPattern.isValid()) {
LOG_MESSAGE(QString("Invalid regex pattern: %1").arg(symbolName));
return results;
}
} else if (useWildcard) {
QString regexPattern = QRegularExpression::wildcardToRegularExpression(symbolName);
QRegularExpression::PatternOptions options = QRegularExpression::NoPatternOption;
if (!caseSensitive) {
options |= QRegularExpression::CaseInsensitiveOption;
}
searchPattern.setPattern(regexPattern);
searchPattern.setPatternOptions(options);
}
const CPlusPlus::Snapshot snapshot = modelManager->snapshot();
CPlusPlus::Overview overview;
for (auto it = snapshot.begin(); it != snapshot.end(); ++it) {
CPlusPlus::Document::Ptr doc = it.value();
if (!doc || !doc->globalNamespace()) {
continue;
}
QString filePath = doc->filePath().toUserOutput();
auto project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath));
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
continue;
}
searchInScope(
doc->globalNamespace(),
symbolName,
type,
scopeFilter,
filePath,
overview,
QString(),
caseSensitive,
useRegex,
useWildcard,
useRegex || useWildcard ? &searchPattern : nullptr,
results);
}
return results;
}
void FindSymbolTool::searchInScope(
CPlusPlus::Scope *scope,
const QString &symbolName,
SymbolType searchType,
const QString &scopeFilter,
const QString &filePath,
const CPlusPlus::Overview &overview,
const QString &currentScope,
bool caseSensitive,
bool useRegex,
bool useWildcard,
QRegularExpression *searchPattern,
QList<SymbolInfo> &results) const
{
if (!scope) {
return;
}
for (unsigned i = 0; i < scope->memberCount(); ++i) {
CPlusPlus::Symbol *symbol = scope->memberAt(i);
if (!symbol || !symbol->name()) {
continue;
}
QString currentSymbolName = overview.prettyName(symbol->name());
QString fullScope = buildFullScope(currentScope, currentSymbolName);
if (matchesSymbolName(currentSymbolName,
symbolName,
caseSensitive,
useRegex,
useWildcard,
searchPattern)
&& matchesType(symbol, searchType)) {
if (scopeFilter.isEmpty() || matchesScopeFilter(currentScope, scopeFilter)) {
results.append(createSymbolInfo(symbol, filePath, currentScope, overview));
}
}
if (symbol->asNamespace() || symbol->asClass() || symbol->asEnum()) {
searchInScope(symbol->asScope(),
symbolName,
searchType,
scopeFilter,
filePath,
overview,
fullScope,
caseSensitive,
useRegex,
useWildcard,
searchPattern,
results);
}
}
}
bool FindSymbolTool::matchesScopeFilter(const QString &fullScope, const QString &scopeFilter) const
{
if (scopeFilter.isEmpty()) {
return true;
}
// Match if the full scope contains the filter
// E.g., "MyNamespace::MyClass" matches filter "MyNamespace" or "MyClass"
return fullScope.contains(scopeFilter) || fullScope.endsWith(scopeFilter);
}
QString FindSymbolTool::buildFullScope(const QString &currentScope, const QString &symbolName) const
{
if (currentScope.isEmpty()) {
return symbolName;
}
return currentScope + "::" + symbolName;
}
bool FindSymbolTool::matchesSymbolName(
const QString &symbolName,
const QString &searchPattern,
bool caseSensitive,
bool useRegex,
bool useWildcard,
QRegularExpression *regex) const
{
if (useRegex || useWildcard) {
// Use regex pattern matching
if (regex && regex->isValid()) {
return regex->match(symbolName).hasMatch();
}
return false;
}
// Exact match (with optional case sensitivity)
if (caseSensitive) {
return symbolName == searchPattern;
} else {
return symbolName.compare(searchPattern, Qt::CaseInsensitive) == 0;
}
}
bool FindSymbolTool::matchesType(CPlusPlus::Symbol *symbol, SymbolType type) const
{
if (type == SymbolType::All) {
return true;
}
switch (type) {
case SymbolType::Class:
return symbol->asClass() != nullptr;
case SymbolType::Function:
return symbol->asFunction() != nullptr;
case SymbolType::Enum:
return symbol->asEnum() != nullptr;
case SymbolType::Namespace:
return symbol->asNamespace() != nullptr;
case SymbolType::Variable:
return symbol->asDeclaration() != nullptr && !symbol->type()->asFunctionType();
case SymbolType::Typedef:
return symbol->asTypenameArgument() != nullptr
|| (symbol->asDeclaration() && symbol->asDeclaration()->isTypedef());
default:
return false;
}
}
FindSymbolTool::SymbolInfo FindSymbolTool::createSymbolInfo(
CPlusPlus::Symbol *symbol,
const QString &filePath,
const QString &fullScope,
const CPlusPlus::Overview &overview) const
{
Q_UNUSED(fullScope)
Q_UNUSED(overview)
SymbolInfo info;
info.filePath = filePath;
info.line = symbol->line();
// Determine symbol type
if (symbol->asClass()) {
info.type = SymbolType::Class;
} else if (symbol->asFunction()) {
info.type = SymbolType::Function;
} else if (symbol->asEnum()) {
info.type = SymbolType::Enum;
} else if (symbol->asNamespace()) {
info.type = SymbolType::Namespace;
} else if (auto *declaration = symbol->asDeclaration()) {
if (declaration->isTypedef()) {
info.type = SymbolType::Typedef;
} else {
info.type = SymbolType::Variable;
}
} else {
info.type = SymbolType::All;
}
return info;
}
QString FindSymbolTool::formatResults(const QList<SymbolInfo> &symbols) const
{
QString output = QString("Found %1 symbol(s):\n\n").arg(symbols.size());
for (const SymbolInfo &info : symbols) {
output += QString("Path: %1\nLine:%2\n").arg(info.filePath).arg(info.line);
}
return output.trimmed();
}
} // namespace QodeAssist::Tools

View File

@ -1,104 +0,0 @@
/*
* 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>
#include <QSharedPointer>
namespace CPlusPlus {
class Symbol;
class Scope;
class Overview;
class Document;
} // namespace CPlusPlus
namespace QodeAssist::Tools {
class FindSymbolTool : public LLMCore::BaseTool
{
Q_OBJECT
public:
explicit FindSymbolTool(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:
enum class SymbolType { All, Class, Function, Enum, Variable, Typedef, Namespace };
struct SymbolInfo
{
QString filePath;
int line;
SymbolType type;
};
QList<SymbolInfo> findSymbols(
const QString &symbolName,
SymbolType type,
const QString &scopeFilter,
bool caseSensitive,
bool useRegex,
bool useWildcard) const;
QString formatResults(const QList<SymbolInfo> &symbols) const;
SymbolType parseSymbolType(const QString &typeStr) const;
void searchInScope(
CPlusPlus::Scope *scope,
const QString &symbolName,
SymbolType searchType,
const QString &scopeFilter,
const QString &filePath,
const CPlusPlus::Overview &overview,
const QString &currentScope,
bool caseSensitive,
bool useRegex,
bool useWildcard,
QRegularExpression *searchPattern,
QList<SymbolInfo> &results) const;
bool matchesType(CPlusPlus::Symbol *symbol, SymbolType type) const;
bool matchesScopeFilter(const QString &fullScope, const QString &scopeFilter) const;
bool matchesSymbolName(
const QString &symbolName,
const QString &searchPattern,
bool caseSensitive,
bool useRegex,
bool useWildcard,
QRegularExpression *regex) const;
SymbolInfo createSymbolInfo(
CPlusPlus::Symbol *symbol,
const QString &filePath,
const QString &fullScope,
const CPlusPlus::Overview &overview) const;
QString buildFullScope(const QString &currentScope, const QString &symbolName) const;
Context::IgnoreManager *m_ignoreManager;
};
} // namespace QodeAssist::Tools

358
tools/ProjectSearchTool.cpp Normal file
View File

@ -0,0 +1,358 @@
/*
* 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 "ProjectSearchTool.hpp"
#include "ToolExceptions.hpp"
#include <cplusplus/Overview.h>
#include <cplusplus/Scope.h>
#include <cplusplus/Symbols.h>
#include <cppeditor/cppmodelmanager.h>
#include <logger/Logger.hpp>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include <QTextStream>
#include <QtConcurrent>
namespace QodeAssist::Tools {
ProjectSearchTool::ProjectSearchTool(QObject *parent)
: BaseTool(parent)
, m_ignoreManager(new Context::IgnoreManager(this))
{}
QString ProjectSearchTool::name() const
{
return "search_project";
}
QString ProjectSearchTool::stringName() const
{
return "Searching in project";
}
QString ProjectSearchTool::description() const
{
return "Search project for text content or C++ symbols. "
"Text mode: finds text patterns in files. "
"Symbol mode: finds C++ definitions (classes, functions, etc).";
}
QJsonObject ProjectSearchTool::getDefinition(LLMCore::ToolSchemaFormat format) const
{
QJsonObject properties;
properties["query"]
= QJsonObject{{"type", "string"}, {"description", "Text or symbol name to search for"}};
properties["search_type"] = QJsonObject{
{"type", "string"},
{"enum", QJsonArray{"text", "symbol"}},
{"description", "Search mode: 'text' for content, 'symbol' for C++ definitions"}};
properties["symbol_type"] = QJsonObject{
{"type", "string"},
{"enum", QJsonArray{"all", "class", "function", "enum", "variable", "namespace"}},
{"description", "Symbol type filter (symbol mode only)"}};
properties["case_sensitive"]
= QJsonObject{{"type", "boolean"}, {"description", "Case-sensitive search"}};
properties["use_regex"]
= QJsonObject{{"type", "boolean"}, {"description", "Use regex patterns"}};
properties["whole_words"]
= QJsonObject{{"type", "boolean"}, {"description", "Match whole words only (text mode)"}};
properties["file_pattern"] = QJsonObject{
{"type", "string"}, {"description", "File filter pattern (e.g., '*.cpp', '*.h')"}};
QJsonObject definition;
definition["type"] = "object";
definition["properties"] = properties;
definition["required"] = QJsonArray{"query", "search_type"};
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 ProjectSearchTool::requiredPermissions() const
{
return LLMCore::ToolPermission::FileSystemRead;
}
QFuture<QString> ProjectSearchTool::executeAsync(const QJsonObject &input)
{
return QtConcurrent::run([this, input]() -> QString {
QString query = input["query"].toString().trimmed();
if (query.isEmpty()) {
throw ToolInvalidArgument("Query parameter is required");
}
QString searchTypeStr = input["search_type"].toString();
if (searchTypeStr != "text" && searchTypeStr != "symbol") {
throw ToolInvalidArgument("search_type must be 'text' or 'symbol'");
}
SearchType searchType = (searchTypeStr == "symbol") ? SearchType::Symbol : SearchType::Text;
QList<SearchResult> results;
if (searchType == SearchType::Text) {
bool caseSensitive = input["case_sensitive"].toBool(false);
bool useRegex = input["use_regex"].toBool(false);
bool wholeWords = input["whole_words"].toBool(false);
QString filePattern = input["file_pattern"].toString();
results = searchText(query, caseSensitive, useRegex, wholeWords, filePattern);
} else {
SymbolType symbolType = parseSymbolType(input["symbol_type"].toString());
bool caseSensitive = input["case_sensitive"].toBool(false);
bool useRegex = input["use_regex"].toBool(false);
results = searchSymbols(query, symbolType, caseSensitive, useRegex);
}
if (results.isEmpty()) {
return QString("No matches found for '%1'").arg(query);
}
return formatResults(results, query);
});
}
QList<ProjectSearchTool::SearchResult> ProjectSearchTool::searchText(
const QString &query,
bool caseSensitive,
bool useRegex,
bool wholeWords,
const QString &filePattern)
{
QList<SearchResult> results;
auto projects = ProjectExplorer::ProjectManager::projects();
if (projects.isEmpty())
return results;
QRegularExpression searchRegex;
if (useRegex) {
QRegularExpression::PatternOptions options = QRegularExpression::MultilineOption;
if (!caseSensitive)
options |= QRegularExpression::CaseInsensitiveOption;
searchRegex.setPattern(query);
searchRegex.setPatternOptions(options);
if (!searchRegex.isValid())
return results;
}
QRegularExpression fileFilter;
if (!filePattern.isEmpty()) {
fileFilter.setPattern(QRegularExpression::wildcardToRegularExpression(filePattern));
}
for (auto project : projects) {
if (!project)
continue;
auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
QString projectDir = project->projectDirectory().path();
for (const auto &filePath : projectFiles) {
QString absolutePath = filePath.path();
if (m_ignoreManager->shouldIgnore(absolutePath, project))
continue;
if (!filePattern.isEmpty()) {
QFileInfo fileInfo(absolutePath);
if (!fileFilter.match(fileInfo.fileName()).hasMatch())
continue;
}
QFile file(absolutePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
continue;
QTextStream stream(&file);
int lineNumber = 0;
while (!stream.atEnd()) {
lineNumber++;
QString line = stream.readLine();
bool matched = false;
if (useRegex) {
matched = searchRegex.match(line).hasMatch();
} else {
auto cs = caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
if (wholeWords) {
QRegularExpression wordRegex(
QString("\\b%1\\b").arg(QRegularExpression::escape(query)),
caseSensitive ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption);
matched = wordRegex.match(line).hasMatch();
} else {
matched = line.contains(query, cs);
}
}
if (matched) {
SearchResult result;
result.filePath = absolutePath;
result.relativePath = QDir(projectDir).relativeFilePath(absolutePath);
result.content = line.trimmed();
result.lineNumber = lineNumber;
results.append(result);
}
}
}
}
return results;
}
QList<ProjectSearchTool::SearchResult> ProjectSearchTool::searchSymbols(
const QString &query, SymbolType symbolType, bool caseSensitive, bool useRegex)
{
QList<SearchResult> results;
auto modelManager = CppEditor::CppModelManager::instance();
if (!modelManager)
return results;
QRegularExpression searchRegex;
if (useRegex) {
QRegularExpression::PatternOptions options = caseSensitive
? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption;
searchRegex.setPattern(query);
searchRegex.setPatternOptions(options);
if (!searchRegex.isValid())
return results;
}
CPlusPlus::Overview overview;
auto snapshot = modelManager->snapshot();
for (auto it = snapshot.begin(); it != snapshot.end(); ++it) {
auto document = it.value();
if (!document || !document->globalNamespace())
continue;
QString filePath = document->filePath().path();
if (m_ignoreManager->shouldIgnore(filePath, nullptr))
continue;
auto searchInScope = [&](auto self, CPlusPlus::Scope *scope) -> void {
if (!scope)
return;
for (unsigned i = 0; i < scope->memberCount(); ++i) {
auto symbol = scope->memberAt(i);
if (!symbol || !symbol->name())
continue;
QString symbolName = overview.prettyName(symbol->name());
bool nameMatches = false;
if (useRegex) {
nameMatches = searchRegex.match(symbolName).hasMatch();
} else {
nameMatches = caseSensitive
? symbolName == query
: symbolName.compare(query, Qt::CaseInsensitive) == 0;
}
bool typeMatches = (symbolType == SymbolType::All)
|| (symbolType == SymbolType::Class && symbol->asClass())
|| (symbolType == SymbolType::Function && symbol->asFunction())
|| (symbolType == SymbolType::Enum && symbol->asEnum())
|| (symbolType == SymbolType::Variable && symbol->asDeclaration())
|| (symbolType == SymbolType::Namespace && symbol->asNamespace());
if (nameMatches && typeMatches) {
SearchResult result;
result.filePath = filePath;
auto projects = ProjectExplorer::ProjectManager::projects();
if (!projects.isEmpty()) {
QString projectDir = projects.first()->projectDirectory().path();
result.relativePath = QDir(projectDir).relativeFilePath(filePath);
} else {
result.relativePath = QFileInfo(filePath).fileName();
}
result.content = symbolName;
result.lineNumber = symbol->line();
result.context = overview.prettyType(symbol->type());
results.append(result);
}
if (auto nestedScope = symbol->asScope()) {
self(self, nestedScope);
}
}
};
searchInScope(searchInScope, document->globalNamespace());
}
return results;
}
ProjectSearchTool::SymbolType ProjectSearchTool::parseSymbolType(const QString &typeStr)
{
if (typeStr == "class")
return SymbolType::Class;
if (typeStr == "function")
return SymbolType::Function;
if (typeStr == "enum")
return SymbolType::Enum;
if (typeStr == "variable")
return SymbolType::Variable;
if (typeStr == "namespace")
return SymbolType::Namespace;
return SymbolType::All;
}
QString ProjectSearchTool::formatResults(const QList<SearchResult> &results, const QString &query)
{
QString output = QString("Query: %1\n Found %2 matches:\n\n").arg(query).arg(results.size());
int count = 0;
for (const auto &r : results) {
if (++count > 100) {
output += QString("... and %1 more matches").arg(results.size() - 20);
break;
}
output += QString("%1:%2: %3\n").arg(r.relativePath).arg(r.lineNumber).arg(r.content);
}
return output;
}
} // namespace QodeAssist::Tools

View File

@ -21,42 +21,51 @@
#include <context/IgnoreManager.hpp>
#include <llmcore/BaseTool.hpp>
#include <QFuture>
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::Tools {
class SearchInProjectTool : public LLMCore::BaseTool
class ProjectSearchTool : public LLMCore::BaseTool
{
Q_OBJECT
public:
explicit SearchInProjectTool(QObject *parent = nullptr);
explicit ProjectSearchTool(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;
QFuture<QString> executeAsync(const QJsonObject &input) override;
private:
enum class SearchType { Text, Symbol };
enum class SymbolType { All, Class, Function, Enum, Variable, Namespace };
struct SearchResult
{
QString filePath;
int lineNumber;
QString lineContent;
QString relativePath;
QString content;
int lineNumber = 0;
QString context;
};
QList<SearchResult> searchInFiles(
const QString &searchText,
QList<SearchResult> searchText(
const QString &query,
bool caseSensitive,
bool useRegex,
bool wholeWords,
const QString &filePattern) const;
const QString &filePattern);
QString formatResults(const QList<SearchResult> &results,
int maxResults,
const QString &searchQuery) const;
QList<SearchResult> searchSymbols(
const QString &query, SymbolType symbolType, bool caseSensitive, bool useRegex);
SymbolType parseSymbolType(const QString &typeStr);
QString formatResults(const QList<SearchResult> &results, const QString &query);
Context::IgnoreManager *m_ignoreManager;
};

View File

@ -1,261 +0,0 @@
/*
* 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 "ReadFilesByPathTool.hpp"
#include "ToolExceptions.hpp"
#include <context/ProjectUtils.hpp>
#include <coreplugin/documentmanager.h>
#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 <QJsonDocument>
#include <QJsonObject>
#include <QTextStream>
#include <QtConcurrent>
namespace QodeAssist::Tools {
ReadFilesByPathTool::ReadFilesByPathTool(QObject *parent)
: BaseTool(parent)
, m_ignoreManager(new Context::IgnoreManager(this))
{}
QString ReadFilesByPathTool::name() const
{
return "read_files_by_path";
}
QString ReadFilesByPathTool::stringName() const
{
return {"Reading file(s)"};
}
QString ReadFilesByPathTool::description() const
{
return "Read content of project file(s) by absolute path. "
"Use 'filepath' for single file or 'filepaths' array for multiple files (e.g., .h and .cpp). "
"Files must exist and not be excluded by .qodeassistignore.";
}
QJsonObject ReadFilesByPathTool::getDefinition(LLMCore::ToolSchemaFormat format) const
{
QJsonObject properties;
QJsonObject filepathProperty;
filepathProperty["type"] = "string";
filepathProperty["description"] = "The absolute file path to read (for single file)";
properties["filepath"] = filepathProperty;
QJsonObject filepathsProperty;
filepathsProperty["type"] = "array";
QJsonObject itemsProperty;
itemsProperty["type"] = "string";
filepathsProperty["items"] = itemsProperty;
filepathsProperty["description"] = "Array of absolute file paths to read (for multiple files, "
"e.g., both .h and .cpp)";
properties["filepaths"] = filepathsProperty;
QJsonObject definition;
definition["type"] = "object";
definition["properties"] = properties;
definition["description"] = "Provide either 'filepath' for a single file or 'filepaths' for "
"multiple files";
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 ReadFilesByPathTool::requiredPermissions() const
{
return LLMCore::ToolPermission::FileSystemRead;
}
QFuture<QString> ReadFilesByPathTool::executeAsync(const QJsonObject &input)
{
return QtConcurrent::run([this, input]() -> QString {
QStringList filePaths;
QString singlePath = input["filepath"].toString();
if (!singlePath.isEmpty()) {
filePaths.append(singlePath);
}
if (input.contains("filepaths") && input["filepaths"].isArray()) {
QJsonArray pathsArray = input["filepaths"].toArray();
for (const auto &pathValue : pathsArray) {
QString path = pathValue.toString();
if (!path.isEmpty()) {
filePaths.append(path);
}
}
}
if (filePaths.isEmpty()) {
QString error = "Error: either 'filepath' or 'filepaths' parameter is required";
throw ToolInvalidArgument(error);
}
LOG_MESSAGE(QString("Processing %1 file(s)").arg(filePaths.size()));
QList<FileResult> results;
for (const QString &filePath : filePaths) {
results.append(processFile(filePath));
}
return formatResults(results);
});
}
QString ReadFilesByPathTool::readFileContent(const QString &filePath) const
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
LOG_MESSAGE(QString("Could not open file: %1, error: %2").arg(filePath, file.errorString()));
throw ToolRuntimeError(
QString("Error: Could not open file '%1': %2").arg(filePath, file.errorString()));
}
QTextStream stream(&file);
stream.setAutoDetectUnicode(true);
QString content = stream.readAll();
file.close();
LOG_MESSAGE(QString("Successfully read file: %1, size: %2 bytes, isEmpty: %3")
.arg(filePath)
.arg(content.length())
.arg(content.isEmpty()));
return content;
}
ReadFilesByPathTool::FileResult ReadFilesByPathTool::processFile(const QString &filePath) const
{
FileResult result;
result.path = filePath;
result.success = false;
try {
QFileInfo fileInfo(filePath);
LOG_MESSAGE(QString("Checking file: %1, exists: %2, isFile: %3")
.arg(filePath)
.arg(fileInfo.exists())
.arg(fileInfo.isFile()));
if (!fileInfo.exists() || !fileInfo.isFile()) {
result.error = QString("File does not exist");
return result;
}
QString canonicalPath = fileInfo.canonicalFilePath();
LOG_MESSAGE(QString("Canonical path: %1").arg(canonicalPath));
bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath);
if (!isInProject) {
const auto &settings = Settings::generalSettings();
if (!settings.allowAccessOutsideProject()) {
result.error = QString(
"File is not part of the project. "
"Enable 'Allow file access outside project' in settings "
"to read files outside project scope.");
return result;
}
LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath));
}
auto project = isInProject ? ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(canonicalPath))
: nullptr;
if (isInProject && project && m_ignoreManager->shouldIgnore(canonicalPath, project)) {
result.error = QString("File is excluded by .qodeassistignore");
return result;
}
result.content = readFileContent(canonicalPath);
result.success = true;
result.path = canonicalPath;
} catch (const ToolRuntimeError &e) {
result.error = e.message();
} catch (const std::exception &e) {
result.error = QString("Unexpected error: %1").arg(QString::fromUtf8(e.what()));
}
return result;
}
QString ReadFilesByPathTool::formatResults(const QList<FileResult> &results) const
{
if (results.size() == 1) {
const FileResult &result = results.first();
if (!result.success) {
throw ToolRuntimeError(QString("Error: %1 - %2").arg(result.path, result.error));
}
if (result.content.isEmpty()) {
return QString("File: %1\n\nThe file is empty").arg(result.path);
}
return QString("File: %1\n\nContent:\n%2").arg(result.path, result.content);
}
QStringList output;
int successCount = 0;
for (const FileResult &result : results) {
if (result.success) {
successCount++;
output.append(QString("=== File: %1 ===\n").arg(result.path));
if (result.content.isEmpty()) {
output.append("[Empty file]\n");
} else {
output.append(result.content);
}
output.append("\n");
} else {
output.append(QString("=== File: %1 ===\n").arg(result.path));
output.append(QString("[Error: %1]\n\n").arg(result.error));
}
}
QString summary
= QString("Successfully read %1 of %2 file(s)\n\n").arg(successCount).arg(results.size());
return summary + output.join("");
}
} // namespace QodeAssist::Tools

View File

@ -1,56 +0,0 @@
/*
* 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 ReadFilesByPathTool : public LLMCore::BaseTool
{
Q_OBJECT
public:
explicit ReadFilesByPathTool(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 FileResult
{
QString path;
QString content;
bool success;
QString error;
};
QString readFileContent(const QString &filePath) const;
FileResult processFile(const QString &filePath) const;
QString formatResults(const QList<FileResult> &results) const;
Context::IgnoreManager *m_ignoreManager;
};
} // namespace QodeAssist::Tools

View File

@ -1,309 +0,0 @@
/*
* 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 "SearchInProjectTool.hpp"
#include "ToolExceptions.hpp"
#include <logger/Logger.hpp>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include <QTextStream>
#include <QtConcurrent>
namespace QodeAssist::Tools {
SearchInProjectTool::SearchInProjectTool(QObject *parent)
: BaseTool(parent)
, m_ignoreManager(new Context::IgnoreManager(this))
{}
QString SearchInProjectTool::name() const
{
return "search_in_project";
}
QString SearchInProjectTool::stringName() const
{
return {"Searching in project files"};
}
QString SearchInProjectTool::description() const
{
return "Search for text or regex patterns across project files. "
"Returns matching lines with file paths, line numbers, and context. "
"Supports case-sensitive/insensitive, whole word matching, and file pattern filtering (*.cpp, *.h).";
}
QJsonObject SearchInProjectTool::getDefinition(LLMCore::ToolSchemaFormat format) const
{
QJsonObject properties;
QJsonObject queryProperty;
queryProperty["type"] = "string";
queryProperty["description"] = "Text or regex pattern to search for";
properties["query"] = queryProperty;
QJsonObject caseSensitiveProperty;
caseSensitiveProperty["type"] = "boolean";
caseSensitiveProperty["description"] = "Enable case-sensitive search";
properties["case_sensitive"] = caseSensitiveProperty;
QJsonObject useRegexProperty;
useRegexProperty["type"] = "boolean";
useRegexProperty["description"] = "Treat query as regular expression";
properties["use_regex"] = useRegexProperty;
QJsonObject wholeWordsProperty;
wholeWordsProperty["type"] = "boolean";
wholeWordsProperty["description"] = "Match whole words only";
properties["whole_words"] = wholeWordsProperty;
QJsonObject filePatternProperty;
filePatternProperty["type"] = "string";
filePatternProperty["description"] = "File pattern to filter results (e.g., '*.cpp', '*.h')";
properties["file_pattern"] = filePatternProperty;
QJsonObject maxResultsProperty;
maxResultsProperty["type"] = "integer";
maxResultsProperty["description"] = "Maximum number of results to return (default: 50)";
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 SearchInProjectTool::requiredPermissions() const
{
return LLMCore::ToolPermission::FileSystemRead;
}
QFuture<QString> SearchInProjectTool::executeAsync(const QJsonObject &input)
{
return QtConcurrent::run([this, input]() -> QString {
QString query = input["query"].toString();
if (query.isEmpty()) {
QString error = "Error: query parameter is required";
throw ToolInvalidArgument(error);
}
bool caseSensitive = input["case_sensitive"].toBool(false);
bool useRegex = input["use_regex"].toBool(false);
bool wholeWords = input["whole_words"].toBool(false);
QString filePattern = input["file_pattern"].toString();
int maxResults = input["max_results"].toInt(50);
LOG_MESSAGE(QString("Searching for: '%1' (case_sensitive: %2, regex: %3, whole_words: %4)")
.arg(query)
.arg(caseSensitive)
.arg(useRegex)
.arg(wholeWords));
QList<SearchResult> results
= searchInFiles(query, caseSensitive, useRegex, wholeWords, filePattern);
if (results.isEmpty()) {
return QString("No matches found for '%1'").arg(query);
}
return formatResults(results, maxResults, query);
});
}
QList<SearchInProjectTool::SearchResult> SearchInProjectTool::searchInFiles(
const QString &searchText,
bool caseSensitive,
bool useRegex,
bool wholeWords,
const QString &filePattern) const
{
QList<SearchResult> results;
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
if (projects.isEmpty()) {
LOG_MESSAGE("No projects found");
return results;
}
QRegularExpression searchRegex;
if (useRegex) {
QRegularExpression::PatternOptions options = QRegularExpression::MultilineOption;
if (!caseSensitive) {
options |= QRegularExpression::CaseInsensitiveOption;
}
searchRegex.setPattern(searchText);
searchRegex.setPatternOptions(options);
if (!searchRegex.isValid()) {
LOG_MESSAGE(QString("Invalid regex pattern: %1").arg(searchText));
return results;
}
}
QRegularExpression filePatternRegex;
if (!filePattern.isEmpty()) {
QString pattern = QRegularExpression::wildcardToRegularExpression(filePattern);
filePatternRegex.setPattern(pattern);
}
for (auto project : projects) {
if (!project)
continue;
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
for (const auto &filePath : projectFiles) {
QString absolutePath = filePath.path();
if (m_ignoreManager->shouldIgnore(absolutePath, project)) {
continue;
}
if (!filePattern.isEmpty()) {
QFileInfo fileInfo(absolutePath);
if (!filePatternRegex.match(fileInfo.fileName()).hasMatch()) {
continue;
}
}
QFile file(absolutePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
continue;
}
QTextStream stream(&file);
stream.setAutoDetectUnicode(true);
int lineNumber = 0;
while (!stream.atEnd()) {
lineNumber++;
QString line = stream.readLine();
bool matched = false;
if (useRegex) {
matched = searchRegex.match(line).hasMatch();
} else {
Qt::CaseSensitivity cs = caseSensitive ? Qt::CaseSensitive
: Qt::CaseInsensitive;
if (wholeWords) {
QRegularExpression wordRegex(
QString("\\b%1\\b").arg(QRegularExpression::escape(searchText)),
caseSensitive ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption);
matched = wordRegex.match(line).hasMatch();
} else {
matched = line.contains(searchText, cs);
}
}
if (matched) {
SearchResult result;
result.filePath = absolutePath;
result.lineNumber = lineNumber;
result.lineContent = line.trimmed();
QString context;
long long currentPos = stream.pos();
stream.seek(0);
int contextLineNum = 0;
while (contextLineNum < lineNumber - 1 && !stream.atEnd()) {
stream.readLine();
contextLineNum++;
}
QStringList contextLines;
for (int i = qMax(1, lineNumber - 2); i < lineNumber; ++i) {
if (!stream.atEnd()) {
contextLines.append(stream.readLine().trimmed());
}
}
if (!contextLines.isEmpty()) {
result.context = contextLines.join("\n");
}
stream.seek(currentPos);
results.append(result);
}
}
file.close();
}
}
return results;
}
QString SearchInProjectTool::formatResults(const QList<SearchResult> &results,
int maxResults,
const QString &searchQuery) const
{
QString output = QString("Search query: '%1'\n").arg(searchQuery);
output += QString("Found %1 matches:\n\n").arg(results.size());
int displayCount = qMin(results.size(), maxResults);
for (int i = 0; i < displayCount; ++i) {
const SearchResult &result = results[i];
output += QString("%1:%2\n").arg(result.filePath).arg(result.lineNumber);
output += QString(" %1\n").arg(result.lineContent);
if (!result.context.isEmpty()) {
output += QString(" Context:\n");
for (const QString &contextLine : result.context.split('\n')) {
output += QString(" %1\n").arg(contextLine);
}
}
output += "\n";
}
if (results.size() > maxResults) {
output += QString("... and %1 more matches\n").arg(results.size() - maxResults);
}
return output.trimmed();
}
} // namespace QodeAssist::Tools

View File

@ -26,13 +26,11 @@
#include "BuildProjectTool.hpp"
#include "CreateNewFileTool.hpp"
#include "FindFileTool.hpp"
#include "FindSymbolTool.hpp"
#include "FindAndReadFileTool.hpp"
#include "GetIssuesListTool.hpp"
#include "ListProjectFilesTool.hpp"
#include "ReadFilesByPathTool.hpp"
#include "ProjectSearchTool.hpp"
#include "ReadVisibleFilesTool.hpp"
#include "SearchInProjectTool.hpp"
namespace QodeAssist::Tools {
@ -45,14 +43,12 @@ ToolsFactory::ToolsFactory(QObject *parent)
void ToolsFactory::registerTools()
{
registerTool(new ReadVisibleFilesTool(this));
registerTool(new ReadFilesByPathTool(this));
registerTool(new ListProjectFilesTool(this));
registerTool(new SearchInProjectTool(this));
registerTool(new GetIssuesListTool(this));
registerTool(new FindSymbolTool(this));
registerTool(new FindFileTool(this));
registerTool(new CreateNewFileTool(this));
registerTool(new BuildProjectTool(this));
registerTool(new ProjectSearchTool(this));
registerTool(new FindAndReadFileTool(this));
LOG_MESSAGE(QString("Registered %1 tools").arg(m_tools.size()));
}