/*
* 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