diff --git a/CMakeLists.txt b/CMakeLists.txt
index 874b784..a4342d8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
diff --git a/tools/FindAndReadFileTool.cpp b/tools/FindAndReadFileTool.cpp
new file mode 100644
index 0000000..a54b324
--- /dev/null
+++ b/tools/FindAndReadFileTool.cpp
@@ -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 .
+ */
+
+#include "FindAndReadFileTool.hpp"
+#include "ToolExceptions.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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 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 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 &matches,
+ int maxResults,
+ int ¤tDepth,
+ int maxDepth)
+{
+ if (currentDepth >= maxDepth || matches.size() >= maxResults)
+ return;
+
+ currentDepth++;
+ QDir dir(dirPath);
+ if (!dir.exists()) {
+ currentDepth--;
+ return;
+ }
+
+ auto entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const auto &entry : entries) {
+ if (matches.size() >= maxResults)
+ break;
+
+ QString absolutePath = entry.absoluteFilePath();
+ if (m_ignoreManager->shouldIgnore(absolutePath, project))
+ continue;
+
+ QString fileName = entry.fileName();
+
+ if (entry.isDir()) {
+ searchInFileSystem(
+ absolutePath,
+ query,
+ projectName,
+ projectDir,
+ project,
+ matches,
+ maxResults,
+ currentDepth,
+ maxDepth);
+ continue;
+ }
+
+ QString lowerFileName = fileName.toLower();
+ QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
+ QString lowerRelativePath = relativePath.toLower();
+
+ FileMatch match;
+ match.absolutePath = absolutePath;
+ match.relativePath = relativePath;
+ match.projectName = projectName;
+
+ if (lowerFileName == query) {
+ match.matchType = MatchType::ExactName;
+ matches.append(match);
+ } else if (lowerRelativePath.contains(query)) {
+ match.matchType = MatchType::PathMatch;
+ matches.append(match);
+ } else if (lowerFileName.contains(query)) {
+ match.matchType = MatchType::PartialName;
+ matches.append(match);
+ }
+ }
+
+ currentDepth--;
+}
+
+bool FindAndReadFileTool::matchesFilePattern(const QString &fileName, const QString &pattern) const
+{
+ if (pattern.isEmpty())
+ return true;
+
+ if (pattern.startsWith("*.")) {
+ QString extension = pattern.mid(1);
+ return fileName.endsWith(extension, Qt::CaseInsensitive);
+ }
+
+ return fileName.compare(pattern, Qt::CaseInsensitive) == 0;
+}
+
+QString FindAndReadFileTool::readFileContent(const QString &filePath) const
+{
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return QString();
+ }
+
+ QString canonicalPath = QFileInfo(filePath).canonicalFilePath();
+ bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath);
+
+ if (!isInProject) {
+ const auto &settings = Settings::generalSettings();
+ if (!settings.allowAccessOutsideProject()) {
+ LOG_MESSAGE(QString("Access denied to file outside project: %1").arg(canonicalPath));
+ return QString();
+ }
+ LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath));
+ }
+
+ QTextStream stream(&file);
+ stream.setAutoDetectUnicode(true);
+ QString content = stream.readAll();
+
+ return content;
+}
+
+QString FindAndReadFileTool::formatResult(const FileMatch &match, bool readContent) const
+{
+ QString 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
diff --git a/tools/FindFileTool.hpp b/tools/FindAndReadFileTool.hpp
similarity index 56%
rename from tools/FindFileTool.hpp
rename to tools/FindAndReadFileTool.hpp
index 7262e62..6b98bdf 100644
--- a/tools/FindFileTool.hpp
+++ b/tools/FindAndReadFileTool.hpp
@@ -21,54 +21,60 @@
#include
#include
+#include
+#include
+#include
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 executeAsync(const QJsonObject &input = QJsonObject()) override;
+ QFuture 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(matchType) < static_cast(other.matchType);
}
};
- QList findMatchingFiles(const QString &query, int maxResults) const;
- void searchInFileSystem(const QString &dirPath,
- const QString &query,
- const QString &projectName,
- const QString &projectDir,
- ProjectExplorer::Project *project,
- QList &matches,
- int maxResults,
- int ¤tDepth,
- int maxDepth = 10) const;
- QString formatResults(const QList &matches, int totalFound, int maxResults) const;
+ FileMatch findBestMatch(const QString &query, const QString &filePattern, int maxResults);
+ void searchInFileSystem(
+ const QString &dirPath,
+ const QString &query,
+ const QString &projectName,
+ const QString &projectDir,
+ ProjectExplorer::Project *project,
+ QList &matches,
+ int maxResults,
+ int ¤tDepth,
+ int maxDepth = 5);
bool matchesFilePattern(const QString &fileName, const QString &pattern) const;
+ QString readFileContent(const QString &filePath) const;
+ QString formatResult(const FileMatch &match, bool readContent) const;
- static constexpr int DEFAULT_MAX_RESULTS = 50;
Context::IgnoreManager *m_ignoreManager;
};
diff --git a/tools/FindFileTool.cpp b/tools/FindFileTool.cpp
deleted file mode 100644
index a83bfd2..0000000
--- a/tools/FindFileTool.cpp
+++ /dev/null
@@ -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 .
- */
-
-#include "FindFileTool.hpp"
-#include "ToolExceptions.hpp"
-
-#include
-#include
-#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 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 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 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("")
- : 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;
- }
- }
-
- // 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 &matches,
- int maxResults,
- int ¤tDepth,
- 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 &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
diff --git a/tools/FindSymbolTool.cpp b/tools/FindSymbolTool.cpp
deleted file mode 100644
index b949116..0000000
--- a/tools/FindSymbolTool.cpp
+++ /dev/null
@@ -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 .
- */
-
-#include "FindSymbolTool.hpp"
-#include "ToolExceptions.hpp"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-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 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 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::findSymbols(
- const QString &symbolName,
- SymbolType type,
- const QString &scopeFilter,
- bool caseSensitive,
- bool useRegex,
- bool useWildcard) const
-{
- QList 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 ¤tScope,
- bool caseSensitive,
- bool useRegex,
- bool useWildcard,
- QRegularExpression *searchPattern,
- QList &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 ¤tScope, 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 &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
diff --git a/tools/FindSymbolTool.hpp b/tools/FindSymbolTool.hpp
deleted file mode 100644
index fe585aa..0000000
--- a/tools/FindSymbolTool.hpp
+++ /dev/null
@@ -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 .
- */
-
-#pragma once
-
-#include
-#include
-#include
-
-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 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 findSymbols(
- const QString &symbolName,
- SymbolType type,
- const QString &scopeFilter,
- bool caseSensitive,
- bool useRegex,
- bool useWildcard) const;
- QString formatResults(const QList &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 ¤tScope,
- bool caseSensitive,
- bool useRegex,
- bool useWildcard,
- QRegularExpression *searchPattern,
- QList &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 ¤tScope, const QString &symbolName) const;
-
- Context::IgnoreManager *m_ignoreManager;
-};
-
-} // namespace QodeAssist::Tools
diff --git a/tools/ProjectSearchTool.cpp b/tools/ProjectSearchTool.cpp
new file mode 100644
index 0000000..a1bfc65
--- /dev/null
+++ b/tools/ProjectSearchTool.cpp
@@ -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 .
+ */
+
+#include "ProjectSearchTool.hpp"
+#include "ToolExceptions.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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 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 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::searchText(
+ const QString &query,
+ bool caseSensitive,
+ bool useRegex,
+ bool wholeWords,
+ const QString &filePattern)
+{
+ QList 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::searchSymbols(
+ const QString &query, SymbolType symbolType, bool caseSensitive, bool useRegex)
+{
+ QList 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 &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
diff --git a/tools/SearchInProjectTool.hpp b/tools/ProjectSearchTool.hpp
similarity index 62%
rename from tools/SearchInProjectTool.hpp
rename to tools/ProjectSearchTool.hpp
index 6206c4a..40874ad 100644
--- a/tools/SearchInProjectTool.hpp
+++ b/tools/ProjectSearchTool.hpp
@@ -21,42 +21,51 @@
#include
#include
+#include
+#include
+#include
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 executeAsync(const QJsonObject &input = QJsonObject()) override;
+ QFuture 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 searchInFiles(
- const QString &searchText,
+ QList searchText(
+ const QString &query,
bool caseSensitive,
bool useRegex,
bool wholeWords,
- const QString &filePattern) const;
+ const QString &filePattern);
- QString formatResults(const QList &results,
- int maxResults,
- const QString &searchQuery) const;
+ QList searchSymbols(
+ const QString &query, SymbolType symbolType, bool caseSensitive, bool useRegex);
+
+ SymbolType parseSymbolType(const QString &typeStr);
+ QString formatResults(const QList &results, const QString &query);
Context::IgnoreManager *m_ignoreManager;
};
diff --git a/tools/ReadFilesByPathTool.cpp b/tools/ReadFilesByPathTool.cpp
deleted file mode 100644
index 3b8285e..0000000
--- a/tools/ReadFilesByPathTool.cpp
+++ /dev/null
@@ -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 .
- */
-
-#include "ReadFilesByPathTool.hpp"
-#include "ToolExceptions.hpp"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-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 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 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 &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
diff --git a/tools/ReadFilesByPathTool.hpp b/tools/ReadFilesByPathTool.hpp
deleted file mode 100644
index 2ce8673..0000000
--- a/tools/ReadFilesByPathTool.hpp
+++ /dev/null
@@ -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 .
- */
-
-#pragma once
-
-#include
-#include
-
-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 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 &results) const;
- Context::IgnoreManager *m_ignoreManager;
-};
-
-} // namespace QodeAssist::Tools
diff --git a/tools/SearchInProjectTool.cpp b/tools/SearchInProjectTool.cpp
deleted file mode 100644
index 5bc2025..0000000
--- a/tools/SearchInProjectTool.cpp
+++ /dev/null
@@ -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 .
- */
-
-#include "SearchInProjectTool.hpp"
-#include "ToolExceptions.hpp"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-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 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 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::searchInFiles(
- const QString &searchText,
- bool caseSensitive,
- bool useRegex,
- bool wholeWords,
- const QString &filePattern) const
-{
- QList results;
-
- QList 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 &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
diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp
index 70d5135..3d9b343 100644
--- a/tools/ToolsFactory.cpp
+++ b/tools/ToolsFactory.cpp
@@ -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()));
}