mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-10-24 12:57:24 -04:00
feat: Add searching tool
This commit is contained in:
317
tools/SearchInProjectTool.cpp
Normal file
317
tools/SearchInProjectTool.cpp
Normal file
@ -0,0 +1,317 @@
|
||||
/*
|
||||
* 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 <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 patterns across all project files. Supports:\n"
|
||||
"- Plain text search (default)\n"
|
||||
"- Regular expression patterns\n"
|
||||
"- Case-sensitive/insensitive search\n"
|
||||
"- Whole word matching\n"
|
||||
"- File pattern filtering (e.g., '*.cpp', '*.h')\n"
|
||||
"Returns matching lines with file paths, line numbers, and surrounding context.\n"
|
||||
"Input parameters:\n"
|
||||
"- 'query' (required): Text or regex pattern to search for\n"
|
||||
"- 'case_sensitive' (optional): true for case-sensitive search, default is false\n"
|
||||
"- 'use_regex' (optional): true to treat query as regex pattern, default is false\n"
|
||||
"- 'whole_words' (optional): true to match whole words only, default is false\n"
|
||||
"- 'file_pattern' (optional): Filter files by pattern (e.g., '*.cpp'), default is all "
|
||||
"files\n"
|
||||
"- 'max_results' (optional): Maximum number of results to return, default is 50";
|
||||
}
|
||||
|
||||
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 std::invalid_argument(error.toStdString());
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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 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
|
||||
62
tools/SearchInProjectTool.hpp
Normal file
62
tools/SearchInProjectTool.hpp
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 SearchInProjectTool : public LLMCore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SearchInProjectTool(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 SearchResult
|
||||
{
|
||||
QString filePath;
|
||||
int lineNumber;
|
||||
QString lineContent;
|
||||
QString context;
|
||||
};
|
||||
|
||||
QList<SearchResult> searchInFiles(
|
||||
const QString &searchText,
|
||||
bool caseSensitive,
|
||||
bool useRegex,
|
||||
bool wholeWords,
|
||||
const QString &filePattern) const;
|
||||
|
||||
QString formatResults(const QList<SearchResult> &results, int maxResults) const;
|
||||
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
@ -27,6 +27,7 @@
|
||||
#include "ListProjectFilesTool.hpp"
|
||||
#include "ReadProjectFileByNameTool.hpp"
|
||||
#include "ReadVisibleFilesTool.hpp"
|
||||
#include "SearchInProjectTool.hpp"
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
@ -41,6 +42,7 @@ void ToolsFactory::registerTools()
|
||||
registerTool(new ReadVisibleFilesTool(this));
|
||||
registerTool(new ReadProjectFileByNameTool(this));
|
||||
registerTool(new ListProjectFilesTool(this));
|
||||
registerTool(new SearchInProjectTool(this));
|
||||
|
||||
LOG_MESSAGE(QString("Registered %1 tools").arg(m_tools.size()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user