mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-11-13 05:22:49 -05:00
refactor: Optimize searching tools
refactor: Merge read and find tool
This commit is contained in:
@ -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
|
||||
|
||||
359
tools/FindAndReadFileTool.cpp
Normal file
359
tools/FindAndReadFileTool.cpp
Normal 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 ¤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
|
||||
@ -21,54 +21,60 @@
|
||||
|
||||
#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,
|
||||
const QString &query,
|
||||
const QString &projectName,
|
||||
const QString &projectDir,
|
||||
ProjectExplorer::Project *project,
|
||||
QList<FileMatch> &matches,
|
||||
int maxResults,
|
||||
int ¤tDepth,
|
||||
int maxDepth = 10) const;
|
||||
QString formatResults(const QList<FileMatch> &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<FileMatch> &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;
|
||||
};
|
||||
|
||||
@ -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 ¤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<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
|
||||
@ -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 ¤tScope,
|
||||
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 ¤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<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
|
||||
@ -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 ¤tScope,
|
||||
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 ¤tScope, const QString &symbolName) const;
|
||||
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
358
tools/ProjectSearchTool.cpp
Normal file
358
tools/ProjectSearchTool.cpp
Normal 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
|
||||
@ -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;
|
||||
};
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user