From 615175bea80242385928b4ae99399fe73d9b3e7c Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:12:47 +0200 Subject: [PATCH] feat: Add file list for ignoring in request for llm (#163) --- LLMClientInterface.cpp | 5 ++ LLMClientInterface.hpp | 2 + QodeAssistClient.cpp | 25 ++++++ QodeAssistClient.hpp | 1 + context/CMakeLists.txt | 1 + context/ContextManager.cpp | 29 +++++++ context/ContextManager.hpp | 6 ++ context/IgnoreManager.cpp | 154 +++++++++++++++++++++++++++++++++++++ context/IgnoreManager.hpp | 54 +++++++++++++ 9 files changed, 277 insertions(+) create mode 100644 context/IgnoreManager.cpp create mode 100644 context/IgnoreManager.hpp diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index 29c1558..26abbc4 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -289,6 +289,11 @@ LLMCore::ContextData LLMClientInterface::prepareContext( return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings); } +Context::ContextManager *LLMClientInterface::contextManager() const +{ + return m_contextManager; +} + void LLMClientInterface::sendCompletionToClient( const QString &completion, const QJsonObject &request, bool isComplete) { diff --git a/LLMClientInterface.hpp b/LLMClientInterface.hpp index 9fba818..ce4c89e 100644 --- a/LLMClientInterface.hpp +++ b/LLMClientInterface.hpp @@ -62,6 +62,8 @@ public: // exposed for tests void sendData(const QByteArray &data) override; + Context::ContextManager *contextManager() const; + protected: void startImpl() override; diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index 9e5ae5a..a5e2422 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -49,6 +49,7 @@ namespace QodeAssist { QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface) : LanguageClient::Client(clientInterface) + , m_llmClient(clientInterface) , m_recentCharCount(0) { setName("QodeAssist"); @@ -73,6 +74,14 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document) if (!isEnabled(project)) return; + if (m_llmClient->contextManager() + ->ignoreManager() + ->shouldIgnore(document->filePath().toUrlishString(), project)) { + LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") + .arg(document->filePath().toUrlishString())); + return; + } + Client::openDocument(document); connect( document, @@ -152,6 +161,14 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor) if (!isEnabled(project)) return; + if (m_llmClient->contextManager() + ->ignoreManager() + ->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) { + LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") + .arg(editor->textDocument()->filePath().toUrlishString())); + return; + } + MultiTextCursor cursor = editor->multiTextCursor(); if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible()) return; @@ -181,6 +198,14 @@ void QodeAssistClient::requestQuickRefactor( if (!isEnabled(project)) return; + if (m_llmClient->contextManager() + ->ignoreManager() + ->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) { + LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") + .arg(editor->textDocument()->filePath().toUrlishString())); + return; + } + if (!m_refactorHandler) { m_refactorHandler = new QuickRefactorHandler(this); connect( diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index bde3523..fb2fde0 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -69,6 +69,7 @@ private: CompletionProgressHandler m_progressHandler; EditorChatButtonHandler m_chatButtonHandler; QuickRefactorHandler *m_refactorHandler{nullptr}; + LLMClientInterface *m_llmClient; }; } // namespace QodeAssist diff --git a/context/CMakeLists.txt b/context/CMakeLists.txt index 5c9212f..6de4dc5 100644 --- a/context/CMakeLists.txt +++ b/context/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(Context STATIC TokenUtils.hpp TokenUtils.cpp ProgrammingLanguage.hpp ProgrammingLanguage.cpp IContextManager.hpp + IgnoreManager.hpp IgnoreManager.cpp ) target_link_libraries(Context diff --git a/context/ContextManager.cpp b/context/ContextManager.cpp index 80a1270..f5d0f2a 100644 --- a/context/ContextManager.cpp +++ b/context/ContextManager.cpp @@ -27,6 +27,7 @@ #include "settings/GeneralSettings.hpp" #include #include +#include #include #include @@ -36,6 +37,7 @@ namespace QodeAssist::Context { ContextManager::ContextManager(QObject *parent) : QObject(parent) + , m_ignoreManager(new IgnoreManager(this)) {} QString ContextManager::readFile(const QString &filePath) const @@ -52,6 +54,13 @@ QList ContextManager::getContentFiles(const QStringList &filePaths) { QList files; for (const QString &path : filePaths) { + auto project = ProjectExplorer::ProjectManager::projectForFile( + Utils::FilePath::fromString(path)); + if (project && m_ignoreManager->shouldIgnore(path, project)) { + LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path)); + continue; + } + ContentFile contentFile = createContentFile(path); files.append(contentFile); } @@ -121,6 +130,14 @@ QList> ContextManager::openedFiles(const QStringList exc continue; auto filePath = textDocument->filePath().toUrlishString(); + + auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath()); + if (project && m_ignoreManager->shouldIgnore(filePath, project)) { + LOG_MESSAGE( + QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath)); + continue; + } + if (!excludeFiles.contains(filePath)) { files.append({filePath, textDocument->plainText()}); } @@ -144,6 +161,13 @@ QString ContextManager::openedFilesContext(const QStringList excludeFiles) if (excludeFiles.contains(filePath)) continue; + auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath()); + if (project && m_ignoreManager->shouldIgnore(filePath, project)) { + LOG_MESSAGE( + QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath)); + continue; + } + context += QString("File: %1\n").arg(filePath); context += textDocument->plainText(); @@ -153,4 +177,9 @@ QString ContextManager::openedFilesContext(const QStringList excludeFiles) return context; } +IgnoreManager *ContextManager::ignoreManager() const +{ + return m_ignoreManager; +} + } // namespace QodeAssist::Context diff --git a/context/ContextManager.hpp b/context/ContextManager.hpp index d359606..a2289ab 100644 --- a/context/ContextManager.hpp +++ b/context/ContextManager.hpp @@ -24,6 +24,7 @@ #include "ContentFile.hpp" #include "IContextManager.hpp" +#include "IgnoreManager.hpp" #include "ProgrammingLanguage.hpp" namespace ProjectExplorer { @@ -49,6 +50,11 @@ public: bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override; QList> openedFiles(const QStringList excludeFiles = QStringList{}) const; QString openedFilesContext(const QStringList excludeFiles = QStringList{}); + + IgnoreManager *ignoreManager() const; + +private: + IgnoreManager *m_ignoreManager; }; } // namespace QodeAssist::Context diff --git a/context/IgnoreManager.cpp b/context/IgnoreManager.cpp new file mode 100644 index 0000000..9e82994 --- /dev/null +++ b/context/IgnoreManager.cpp @@ -0,0 +1,154 @@ +/* + * 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 "IgnoreManager.hpp" + +#include +#include +#include +#include +#include +#include + +#include "logger/Logger.hpp" + +namespace QodeAssist::Context { + +IgnoreManager::IgnoreManager(QObject *parent) + : QObject(parent) +{ + connect(&m_fileWatcher, &QFileSystemWatcher::fileChanged, this, [this](const QString &path) { + for (auto it = m_projectIgnorePatterns.begin(); it != m_projectIgnorePatterns.end(); ++it) { + if (ignoreFilePath(it.key()) == path) { + reloadIgnorePatterns(it.key()); + break; + } + } + }); +} + +IgnoreManager::~IgnoreManager() = default; + +bool IgnoreManager::shouldIgnore(const QString &filePath, ProjectExplorer::Project *project) const +{ + if (!project) + return false; + + if (!m_projectIgnorePatterns.contains(project)) { + const_cast(this)->reloadIgnorePatterns(project); + } + + const QStringList &patterns = m_projectIgnorePatterns[project]; + if (patterns.isEmpty()) + return false; + + QDir projectDir(project->projectDirectory().toUrlishString()); + QString relativePath = projectDir.relativeFilePath(filePath); + + return matchesIgnorePatterns(relativePath, patterns); +} + +void IgnoreManager::reloadIgnorePatterns(ProjectExplorer::Project *project) +{ + if (!project) + return; + + QStringList patterns = loadIgnorePatterns(project); + m_projectIgnorePatterns[project] = patterns; + + QString ignoreFile = ignoreFilePath(project); + if (QFileInfo::exists(ignoreFile)) { + if (m_fileWatcher.files().contains(ignoreFile)) + m_fileWatcher.removePath(ignoreFile); + + m_fileWatcher.addPath(ignoreFile); + + if (!m_projectConnections.contains(project)) { + auto connection = connect(project, &QObject::destroyed, this, [this, project]() { + m_projectIgnorePatterns.remove(project); + m_projectConnections.remove(project); + }); + m_projectConnections[project] = connection; + } + } +} + +QStringList IgnoreManager::loadIgnorePatterns(ProjectExplorer::Project *project) +{ + QStringList patterns; + if (!project) + return patterns; + + QString path = ignoreFilePath(project); + QFile file(path); + + if (!file.exists()) + return patterns; + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + LOG_MESSAGE(QString("Could not open .qodeassistignore file: %1").arg(path)); + return patterns; + } + + QTextStream in(&file); + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + if (!line.isEmpty() && !line.startsWith('#')) + patterns << line; + } + + return patterns; +} + +bool IgnoreManager::matchesIgnorePatterns(const QString &path, const QStringList &patterns) const +{ + for (const QString &pattern : patterns) { + QString regexPattern = QRegularExpression::escape(pattern); + + regexPattern.replace("\\*\\*", ".*"); // ** matches any characters including / + regexPattern.replace("\\*", "[^/]*"); // * matches any characters except / + regexPattern.replace("\\?", "."); // ? matches any single character + + regexPattern = QString("^%1$").arg(regexPattern); + + QRegularExpression regex(regexPattern); + if (regex.match(path).hasMatch()) + return true; + } + + return false; +} + +QString IgnoreManager::ignoreFilePath(ProjectExplorer::Project *project) const +{ + if (!project) { + return QString(); + } + + QString path = project->projectDirectory().toUrlishString() + "/.qodeassist/qodeassistignore"; + + QFileInfo fileInfo(path); + if (!fileInfo.exists() || !fileInfo.isFile()) { + LOG_MESSAGE(QString("File .qodeassistignore not found at path: %1").arg(path)); + } + + return path; +} + +} // namespace QodeAssist::Context diff --git a/context/IgnoreManager.hpp b/context/IgnoreManager.hpp new file mode 100644 index 0000000..3ca8e8d --- /dev/null +++ b/context/IgnoreManager.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +namespace ProjectExplorer { +class Project; +} + +namespace QodeAssist::Context { + +class IgnoreManager : public QObject +{ + Q_OBJECT + +public: + explicit IgnoreManager(QObject *parent = nullptr); + ~IgnoreManager() override; + + bool shouldIgnore(const QString &filePath, ProjectExplorer::Project *project = nullptr) const; + void reloadIgnorePatterns(ProjectExplorer::Project *project); + +private: + QStringList loadIgnorePatterns(ProjectExplorer::Project *project); + bool matchesIgnorePatterns(const QString &path, const QStringList &patterns) const; + QString ignoreFilePath(ProjectExplorer::Project *project) const; + + mutable QMap m_projectIgnorePatterns; + QFileSystemWatcher m_fileWatcher; + QMap m_projectConnections; +}; + +} // namespace QodeAssist::Context