Compare commits

..

67 Commits

Author SHA1 Message Date
dad8ab2bf3 chore: Update plugin to 0.5.12 2025-05-01 15:38:05 +02:00
25a6983de0 refactor: Make connection more async (#182) 2025-05-01 15:35:33 +02:00
4e05abc7d2 feat: Add settings for text format 2025-05-01 00:01:44 +02:00
784529e344 feat: Add chat font settings (#180) 2025-04-30 22:44:59 +02:00
155153a763 fix: Optimize searching unreadable symbols for markdown 2025-04-30 21:23:43 +02:00
9225c0c1a9 fix: Check readable symbols for markdown 2025-04-29 21:55:44 +02:00
43adc95857 feat: Add a floating "copy" button 2025-04-28 09:25:39 +02:00
ee672f2cda refactor: Remove Chat preview scrollbar 2025-04-26 17:29:06 +02:00
a3edb8a577 chore: Upgrade plugin to 0.5.11 2025-04-24 21:46:32 +02:00
407d3b11c0 fix: Change maximum limit of chat tokens 2025-04-24 21:44:49 +02:00
285e739074 refactor: Change base text style render to markdown 2025-04-24 21:38:54 +02:00
f7e748ba7e chore: Upgrade plugin to 0.5.10 2025-04-24 03:24:00 +02:00
acb1306321 fix: Improve detect unclose codeblock 2025-04-24 03:21:19 +02:00
8b38ecc29b feat: Add Chat preview scroll bar 2025-04-24 02:54:21 +02:00
cfb364f033 fix: Correct removing latest item in messages list 2025-04-24 01:32:01 +02:00
2fe6850a06 refactor: Improve textsuggestion working 2025-04-24 01:25:45 +02:00
3e9506ca92 doc: Add description of ignoring files feature to README.md 2025-04-21 09:40:34 +02:00
d24adff0f5 chore: Update plugin version to 0.5.9 2025-04-21 09:18:02 +02:00
447324eb07 feat: Make artifacts name more meaningful (#172) 2025-04-21 09:17:16 +02:00
4ca494cc51 fix: Suggestion symbols count 2025-04-21 09:02:02 +02:00
8a80dbe8f5 fix: Exclude ignore file from attach 2025-04-21 08:37:57 +02:00
2b539bbdeb fix: Remove check ignoring file on open 2025-04-21 08:08:20 +02:00
3f2c146df1 fix: Save codestral api key 2025-04-20 09:57:14 +02:00
9a54f04a0d feat: Add Codestral as separate ai provider (#171) 2025-04-20 09:48:36 +02:00
7a33425d1a feat: Add reset button to clean message list to specific message (#168) 2025-04-18 19:06:17 +02:00
711aa672f2 fix: Increase mac threshold tokens for tokens (#167) 2025-04-18 17:56:48 +02:00
8cb6a2f6d2 fix: Check patterns and remove filewatcher (#166)
* fix: Check patterns and remove filewatcher
* fix: Don't show log for non-existent ignore file
2025-04-18 10:55:46 +02:00
2f9622e23e Update details for Quick Refactor tool in README.md 2025-04-18 08:18:46 +02:00
674b1fecde chore: Upgrade plugin version to 0.5.8 2025-04-17 10:40:10 +02:00
b36d01d2c7 feat: Improve quick refactor dialog (#165) 2025-04-17 10:34:31 +02:00
615175bea8 feat: Add file list for ignoring in request for llm (#163) 2025-04-17 09:12:47 +02:00
7515599acb doc: Add hotkey description for Quick Refactor to README.md 2025-04-14 14:38:12 +02:00
3652d4d5d9 Fix QtCreator version compatibility 2025-04-14 10:54:01 +02:00
75677770b2 Update QtCreator version compatibility README.md 2025-04-14 10:52:45 +02:00
329a1efd5d Update QtCreator version in README.md 2025-04-14 10:51:51 +02:00
27760a3b99 doc: Add example of Quick Refactor to README.md 2025-04-14 01:59:49 +02:00
a93b3cd7f5 chore: Upgrade plugin to QtCreator 16.0.1 (#162) 2025-04-14 01:32:51 +02:00
bacde51d71 feat: Add quick refactor command via context menu (#161)
* feat: Add quick refactor command via context menu
* feat: Add settings for Quick Refactor
2025-04-14 01:01:44 +02:00
418578743a feat: Prepare widget for chat 2025-04-09 18:27:25 +02:00
56e5ef22f1 doc: Remove commercial support from README.md 2025-04-08 08:14:57 +02:00
e90933d713 fix: Add hack for codellama fim models 2025-04-07 18:55:08 +02:00
5b9c67c2d8 doc: Add sharing opened files feature description to README.md 2025-04-04 22:12:36 +02:00
fe84a2a303 chore: Upgrade plugin version to 0.5.6 2025-04-04 18:39:18 +02:00
62de53c306 chore: Update copyrights 2025-04-04 18:01:02 +02:00
7c6a10936c refactor: Simplify update mechanism (#159) 2025-04-04 17:55:36 +02:00
032c9bbbf3 fix: Add input_extra to llama.cpp validator 2025-04-04 17:16:05 +02:00
8906f98038 fix: Rework copyright searching (#158) 2025-04-04 15:29:36 +02:00
5126092449 doc: Minor Update README.md to include a shortcut on Linux (#157)
Added short-cut for Linux KDE Plasma
2025-04-04 14:37:36 +02:00
9d2d70fc63 feat: Add sharing opened files with code completion requests (#156) 2025-04-04 10:38:06 +02:00
ffaf6bd61b feat: Add code completion request progress animation (#153) 2025-04-02 21:00:45 +02:00
79218d8412 refactor: Replace singletone for context manager (#151) 2025-04-01 22:29:45 +02:00
7e6e526ac8 refactor: Removed deprecated api keys fields 2025-03-27 02:12:07 +01:00
80646e2af0 feat: Add additional language for handling to CodeCompletion settings (#150) 2025-03-27 02:07:09 +01:00
5808a892c1 fix: Change expected name in test 2025-03-27 00:41:28 +01:00
d58ff90458 fix: Fixed typo in the use of the project name 2025-03-27 00:34:10 +01:00
7d06ab04dc feat: Add RunQtCreator target 2025-03-27 00:29:23 +01:00
9d40e8ca25 chore: Upgrade plugin to 0.5.5 version 2025-03-20 19:02:11 +01:00
5b16c5403a fix: Add authorization while getting installed models (#142) (#147) 2025-03-20 11:33:38 +01:00
4ddbe0b8b9 feat: Support Ollama authorization via BaseAuth (#145) (#146) 2025-03-20 11:14:50 +01:00
f41e063c02 chore: Upgrade plugin version to 0.5.4 2025-03-17 02:51:31 +01:00
9d7d084448 fix: Wrong template replace to first template (#143) 2025-03-17 02:48:18 +01:00
1ca1ffc629 fix: Remove reading from replay leading to crash (#142) 2025-03-17 01:22:27 +01:00
8419577ae5 fix: Resolve thread-related QNetworkAccessManager issue (#140)
Fixes "QObject: Cannot create children for a parent that is in a different thread" error by creating QNetworkAccessManager in the same thread where it's used, ensuring proper thread affinity for network operations.
2025-03-16 09:47:04 +01:00
91a6a88130 doc: Add default path for installed plugin 2025-03-14 11:35:37 +01:00
be38abc505 chore: upgrade plugin to 0.5.3 (#139) 2025-03-14 10:59:23 +01:00
f2e0afb6b8 fix: Add qml in code handler for processing model answers (#138) 2025-03-14 10:47:30 +01:00
3cf07238fd doc: Update QtC version to 16 2025-03-14 09:32:54 +01:00
164 changed files with 3144 additions and 527 deletions

View File

@ -6,14 +6,14 @@
"llm",
"ai"
],
"compatibility": "Qt 6.8.2",
"compatibility": "Qt 6.8.3",
"platforms": [
"Windows",
"macOS",
"Linux"
],
"license": "GPLv3",
"version": "0.5.2",
"version": "0.5.11",
"status": "draft",
"is_pack": false,
"released_at": null,
@ -25,8 +25,58 @@
},
{
"version": "0.5.2",
"is_latest": true,
"is_latest": false,
"released_at": "2025-03-13T17:00:00Z"
},
{
"version": "0.5.3",
"is_latest": false,
"released_at": "2025-03-14T11:00:00Z"
},
{
"version": "0.5.4",
"is_latest": false,
"released_at": "2025-03-17T03:00:00Z"
},
{
"version": "0.5.5",
"is_latest": false,
"released_at": "2025-03-20T19:00:00Z"
},
{
"version": "0.5.6",
"is_latest": false,
"released_at": "2025-04-04T19:00:00Z"
},
{
"version": "0.5.7",
"is_latest": false,
"released_at": "2025-04-14T01:00:00Z"
},
{
"version": "0.5.8",
"is_latest": false,
"released_at": "2025-04-17T10:00:00Z"
},
{
"version": "0.5.9",
"is_latest": false,
"released_at": "2025-04-21T10:00:00Z"
},
{
"version": "0.5.10",
"is_latest": false,
"released_at": "2025-04-24T10:00:00Z"
},
{
"version": "0.5.11",
"is_latest": false,
"released_at": "2025-04-24T21:00:00Z"
},
{
"version": "0.5.12",
"is_latest": true,
"released_at": "2025-05-01T17:00:00Z"
}
],
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",

View File

@ -12,9 +12,9 @@ on:
env:
PLUGIN_NAME: QodeAssist
QT_VERSION: 6.8.2
QT_CREATOR_VERSION: 16.0.0
QT_CREATOR_VERSION_INTERNAL: 16.0.0
QT_VERSION: 6.8.3
QT_CREATOR_VERSION: 16.0.1
QT_CREATOR_VERSION_INTERNAL: 16.0.1
MACOS_DEPLOYMENT_TARGET: "11.0"
CMAKE_VERSION: "3.29.6"
NINJA_VERSION: "1.12.1"
@ -223,7 +223,7 @@ jobs:
COMMAND python
-u
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
--name "$ENV{PLUGIN_NAME}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
--src .
--build build
--qt-path "${{ steps.qt.outputs.qt_dir }}"
@ -241,8 +241,8 @@ jobs:
- name: Upload
uses: actions/upload-artifact@v4
with:
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
# The json is the same for all platforms, but we need to save one
- name: Upload plugin json

View File

@ -92,6 +92,7 @@ add_qtc_plugin(QodeAssist
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
QodeAssist.qrc
LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp
@ -101,4 +102,27 @@ add_qtc_plugin(QodeAssist
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable
NAMES
qtcreator "Qt Creator"
PATHS
"${QtCreatorCorePath}/../../../bin"
"${QtCreatorCorePath}/../../../MacOS"
NO_DEFAULT_PATH
)
if (QtCreatorExecutable)
add_custom_target(RunQtCreator
COMMAND ${QtCreatorExecutable} -pluginpath $<TARGET_FILE_DIR:QodeAssist>
DEPENDS QodeAssist
)
set_target_properties(RunQtCreator PROPERTIES FOLDER "qtc_runnable")
endif()

View File

@ -17,6 +17,7 @@ qt_add_qml_module(QodeAssistChatView
qml/parts/TopBar.qml
qml/parts/BottomBar.qml
qml/parts/AttachedFilesPlace.qml
RESOURCES
icons/attach-file-light.svg
icons/attach-file-dark.svg

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -124,6 +124,7 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
int lastIndex = 0;
auto blockMatches = codeBlockRegex.globalMatch(content);
bool foundCodeBlock = blockMatches.hasNext();
while (blockMatches.hasNext()) {
auto match = blockMatches.next();
@ -140,7 +141,19 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
if (lastIndex < content.length()) {
QString remainingText = content.mid(lastIndex).trimmed();
if (!remainingText.isEmpty()) {
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
if (unclosedMatch.hasMatch()) {
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
if (!beforeCodeBlock.isEmpty()) {
parts.append({MessagePart::Text, beforeCodeBlock, ""});
}
parts.append(
{MessagePart::Code, unclosedMatch.captured(2).trimmed(), unclosedMatch.captured(1)});
} else if (!remainingText.isEmpty()) {
parts.append({MessagePart::Text, remainingText, ""});
}
}
@ -197,4 +210,16 @@ QString ChatModel::lastMessageId() const
return !m_messages.isEmpty() ? m_messages.last().id : "";
}
void ChatModel::resetModelTo(int index)
{
if (index < 0 || index >= m_messages.size())
return;
if (index < m_messages.size()) {
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
m_messages.remove(index, m_messages.size() - index);
endRemoveRows();
}
}
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -73,6 +73,8 @@ public:
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index);
signals:
void tokensThresholdChanged();
void modelReseted();

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -102,6 +102,31 @@ ChatRootView::ChatRootView(QQuickItem *parent)
}
}
});
connect(
&Settings::chatAssistantSettings().textFontFamily,
&Utils::BaseAspect::changed,
this,
&ChatRootView::textFamilyChanged);
connect(
&Settings::chatAssistantSettings().codeFontFamily,
&Utils::BaseAspect::changed,
this,
&ChatRootView::codeFamilyChanged);
connect(
&Settings::chatAssistantSettings().textFontSize,
&Utils::BaseAspect::changed,
this,
&ChatRootView::textFontSizeChanged);
connect(
&Settings::chatAssistantSettings().codeFontSize,
&Utils::BaseAspect::changed,
this,
&ChatRootView::codeFontSizeChanged);
connect(
&Settings::chatAssistantSettings().textFormat,
&Utils::BaseAspect::changed,
this,
&ChatRootView::textFormatChanged);
updateInputTokensCount();
}
@ -464,12 +489,12 @@ void ChatRootView::updateInputTokensCount()
}
if (!m_attachmentFiles.isEmpty()) {
auto attachFiles = Context::ContextManager::instance().getContentFiles(m_attachmentFiles);
auto attachFiles = m_clientInterface->contextManager()->getContentFiles(m_attachmentFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
}
if (!m_linkedFiles.isEmpty()) {
auto linkFiles = Context::ContextManager::instance().getContentFiles(m_linkedFiles);
auto linkFiles = m_clientInterface->contextManager()->getContentFiles(m_linkedFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
}
@ -510,7 +535,7 @@ void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString();
if (!m_linkedFiles.contains(filePath)) {
if (!m_linkedFiles.contains(filePath) && !shouldIgnoreFileForAttach(document->filePath())) {
m_linkedFiles.append(filePath);
emit linkedFilesChanged();
}
@ -537,4 +562,44 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
}
}
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
{
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString()));
return true;
}
return false;
}
QString ChatRootView::textFontFamily() const
{
return Settings::chatAssistantSettings().textFontFamily.stringValue();
}
QString ChatRootView::codeFontFamily() const
{
return Settings::chatAssistantSettings().codeFontFamily.stringValue();
}
int ChatRootView::codeFontSize() const
{
return Settings::chatAssistantSettings().codeFontSize();
}
int ChatRootView::textFontSize() const
{
return Settings::chatAssistantSettings().textFontSize();
}
int ChatRootView::textFormat() const
{
return Settings::chatAssistantSettings().textFormat();
}
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -38,6 +38,11 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
Q_PROPERTY(QString textFontFamily READ textFontFamily NOTIFY textFamilyChanged FINAL)
Q_PROPERTY(QString codeFontFamily READ codeFontFamily NOTIFY codeFamilyChanged FINAL)
Q_PROPERTY(int codeFontSize READ codeFontSize NOTIFY codeFontSizeChanged FINAL)
Q_PROPERTY(int textFontSize READ textFontSize NOTIFY textFontSizeChanged FINAL)
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
QML_ELEMENT
@ -78,6 +83,14 @@ public:
QString chatFileName() const;
void setRecentFilePath(const QString &filePath);
bool shouldIgnoreFileForAttach(const Utils::FilePath &filePath);
QString textFontFamily() const;
QString codeFontFamily() const;
int codeFontSize() const;
int textFontSize() const;
int textFormat() const;
public slots:
void sendMessage(const QString &message);
@ -94,6 +107,11 @@ signals:
void inputTokensCountChanged();
void isSyncOpenFilesChanged();
void chatFileNameChanged();
void textFamilyChanged();
void codeFamilyChanged();
void codeFontSizeChanged();
void textFontSizeChanged();
void textFormatChanged();
private:
QString getChatsHistoryDir() const;

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -29,4 +29,40 @@ void ChatUtils::copyToClipboard(const QString &text)
QGuiApplication::clipboard()->setText(text);
}
QString ChatUtils::getSafeMarkdownText(const QString &text) const
{
if (text.isEmpty()) {
return text;
}
bool needsSanitization = false;
for (const QChar &ch : text) {
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
needsSanitization = true;
break;
}
}
if (!needsSanitization) {
return text;
}
QString safeText;
safeText.reserve(text.size());
for (QChar ch : text) {
if (ch.isNull()) {
safeText.append(' ');
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
safeText.append(ch);
} else if (ch.isPrint()) {
safeText.append(ch);
} else {
safeText.append(QChar(0xFFFD));
}
}
return safeText;
}
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -34,6 +34,7 @@ public:
: QObject(parent) {};
Q_INVOKABLE void copyToClipboard(const QString &text);
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
};
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -33,7 +33,6 @@
#include <texteditor/texteditor.h>
#include "ChatAssistantSettings.hpp"
#include "ContextManager.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProvidersManager.hpp"
@ -46,6 +45,7 @@ ClientInterface::ClientInterface(
, m_requestHandler(new LLMCore::RequestHandler(this))
, m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, m_contextManager(new Context::ContextManager(this))
{
connect(
m_requestHandler,
@ -73,7 +73,7 @@ void ClientInterface::sendMessage(
{
cancelRequest();
auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
auto attachFiles = m_contextManager->getContentFiles(attachments);
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
auto &chatAssistantSettings = Settings::chatAssistantSettings();
@ -200,7 +200,7 @@ QString ClientInterface::getSystemPromptWithLinkedFiles(
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
@ -209,4 +209,9 @@ QString ClientInterface::getSystemPromptWithLinkedFiles(
return updatedPrompt;
}
Context::ContextManager *ClientInterface::contextManager() const
{
return m_contextManager;
}
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -26,6 +26,7 @@
#include "ChatModel.hpp"
#include "RequestHandler.hpp"
#include "llmcore/IPromptProvider.hpp"
#include <context/ContextManager.hpp>
namespace QodeAssist::Chat {
@ -45,6 +46,8 @@ public:
void clearMessages();
void cancelRequest();
Context::ContextManager *contextManager() const;
signals:
void errorOccurred(const QString &error);
void messageReceivedCompletely();
@ -58,6 +61,7 @@ private:
LLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel;
LLMCore::RequestHandler *m_requestHandler;
Context::ContextManager *m_contextManager;
};
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -27,13 +27,38 @@ Rectangle {
property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model
property string textFontFamily: Qt.application.font.family
property string codeFontFamily: {
switch (Qt.platform.os) {
case "windows":
return "Consolas";
case "osx":
return "Menlo";
case "linux":
return "DejaVu Sans Mono";
default:
return "monospace";
}
}
property int textFontSize: Qt.application.font.pointSize
property int codeFontSize: Qt.application.font.pointSize
property int textFormat: 0
property bool isUserMessage: false
property int messageIndex: -1
property real listViewContentY: 0
signal resetChatToMessage(int index)
height: msgColumn.implicitHeight + 10
radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
HoverHandler {
id: mouse
}
ColumnLayout {
id: msgColumn
@ -77,6 +102,8 @@ Rectangle {
id: codeBlockComponent
CodeBlockComponent {
itemData: msgCreatorDelegate.modelData
blockStart: root.y + msgCreatorDelegate.y
currentContentY: root.listViewContentY
}
}
}
@ -128,16 +155,48 @@ Rectangle {
visible: root.isUserMessage
}
QoAButton {
id: stopButtonId
anchors {
right: parent.right
top: parent.top
}
text: qsTr("ResetTo")
visible: root.isUserMessage && mouse.hovered
onClicked: function() {
root.resetChatToMessage(root.messageIndex)
}
}
component TextComponent : TextBlock {
required property var itemData
height: implicitHeight + 10
verticalAlignment: Text.AlignVCenter
leftPadding: 10
text: itemData.text
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
: itemData.text
font.family: root.textFontFamily
font.pointSize: root.textFontSize
textFormat: {
if (root.textFormat == 0) {
return Text.MarkdownText
} else if (root.textFormat == 1) {
return Text.RichText
} else {
return Text.PlainText
}
}
ChatUtils {
id: utils
}
}
component CodeBlockComponent : CodeBlock {
id: codeblock
required property var itemData
anchors {
left: parent.left
@ -148,5 +207,7 @@ Rectangle {
code: itemData.text
language: itemData.language
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -92,11 +92,25 @@ ChatRootView {
delegate: ChatItem {
required property var model
required property int index
width: ListView.view.width - scroll.width
msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments
isUserMessage: model.roleType === ChatModel.User
messageIndex: index
listViewContentY: chatListView.contentY
textFontFamily: root.textFontFamily
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
textFontSize: root.textFontSize
textFormat: root.textFormat
onResetChatToMessage: function(index) {
messageInput.text = model.content
messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(index)
}
}
header: Item {

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -27,17 +27,25 @@ Rectangle {
property string code: ""
property string language: ""
readonly property string monospaceFont: {
switch (Qt.platform.os) {
case "windows":
return "Consolas";
case "osx":
return "Menlo";
case "linux":
return "DejaVu Sans Mono";
default:
return "monospace";
property real currentContentY: 0
property real blockStart: 0
property alias codeFontFamily: codeText.font.family
property alias codeFontSize: codeText.font.pointSize
readonly property real buttonTopMargin: 5
readonly property real blockEnd: blockStart + root.height
readonly property real maxButtonOffset: Math.max(0, root.height - copyButton.height - buttonTopMargin)
readonly property real buttonPosition: {
if (currentContentY > blockEnd) {
return buttonTopMargin;
}
else if (currentContentY > blockStart) {
let offset = currentContentY - blockStart;
return Math.min(offset, maxButtonOffset);
}
return buttonTopMargin;
}
color: palette.alternateBase
@ -45,7 +53,6 @@ Rectangle {
: Qt.lighter(root.color, 1.3)
border.width: 2
radius: 4
implicitWidth: parent.width
implicitHeight: codeText.implicitHeight + 20
@ -55,14 +62,11 @@ Rectangle {
TextEdit {
id: codeText
anchors.fill: parent
anchors.margins: 10
text: root.code
readOnly: true
selectByMouse: true
font.family: root.monospaceFont
font.pointSize: Qt.application.font.pointSize
color: parent.color.hslLightness > 0.5 ? "black" : "white"
wrapMode: Text.WordWrap
selectionColor: palette.highlight
@ -77,14 +81,20 @@ Rectangle {
text: root.language
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
: Qt.lighter(root.color, 1.1)
font.pointSize: 8
font.pointSize: codeText.font.pointSize - 4
}
QoAButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 5
text: "Copy"
id: copyButton
anchors {
top: parent.top
topMargin: root.buttonPosition
right: parent.right
rightMargin: root.buttonTopMargin
}
text: qsTr("Copy")
onClicked: {
utils.copyToClipboard(root.code)
text = qsTr("Copied")

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -25,7 +25,6 @@ TextEdit {
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
textFormat: Text.StyledText
selectionColor: palette.highlight
color: palette.text
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
@ -19,6 +19,7 @@
*/
#include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp>
#include <QFileInfo>
#include <QHash>
@ -32,6 +33,44 @@ struct LanguageProperties
QVector<QString> fileExtensions;
};
const QVector<LanguageProperties> customLanguagesFromSettings()
{
QVector<LanguageProperties> customLanguages;
const QStringList customLanguagesList = Settings::codeCompletionSettings().customLanguages();
for (const QString &entry : customLanguagesList) {
if (entry.trimmed().isEmpty()) {
continue;
}
QStringList parts = entry.split(',');
if (parts.size() < 4) {
continue;
}
QString name = parts[0].trimmed();
QString commentStyle = parts[1].trimmed();
QStringList modelNamesList = parts[2].trimmed().split(' ', Qt::SkipEmptyParts);
QStringList extensionsList = parts[3].trimmed().split(' ', Qt::SkipEmptyParts);
if (!name.isEmpty() && !commentStyle.isEmpty() && !modelNamesList.isEmpty()
&& !extensionsList.isEmpty()) {
QVector<QString> modelNames;
for (const auto &modelName : modelNamesList) {
modelNames.append(modelName);
}
QVector<QString> extensions;
for (const auto &ext : extensionsList) {
extensions.append(ext);
}
customLanguages.append({name, commentStyle, modelNames, extensions});
}
}
return customLanguages;
}
const QVector<LanguageProperties> &getKnownLanguages()
{
static QVector<LanguageProperties> knownLanguages = {
@ -52,8 +91,11 @@ const QVector<LanguageProperties> &getKnownLanguages()
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
{"perl", "#", {"pl", "perl"}, {"pl"}},
{"hs", "--", {"hs", "haskell"}, {"hs"}},
{"qml", "//", {"qml"}, {"qml"}},
};
knownLanguages.append(customLanguagesFromSettings());
return knownLanguages;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -36,6 +36,7 @@ void ConfigurationManager::init()
{
setupConnections();
updateAllTemplateDescriptions();
checkAllTemplate();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
@ -59,6 +60,26 @@ void ConfigurationManager::updateAllTemplateDescriptions()
updateTemplateDescription(m_generalSettings.caTemplate);
}
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplate.setValue(templ->name());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplate.setValue(templ->name());
}
}
void ConfigurationManager::checkAllTemplate()
{
checkTemplate(m_generalSettings.ccTemplate);
checkTemplate(m_generalSettings.caTemplate);
}
ConfigurationManager::ConfigurationManager(QObject *parent)
: QObject(parent)
, m_generalSettings(Settings::generalSettings())

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -38,6 +38,8 @@ public:
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots:
void selectProvider();

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -24,7 +24,6 @@
#include <QNetworkReply>
#include "CodeHandler.hpp"
#include "context/ContextManager.hpp"
#include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp"
#include "llmcore/PromptTemplateManager.hpp"
@ -51,17 +50,29 @@ LLMClientInterface::LLMClientInterface(
, m_requestHandler(requestHandler)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this))
{
connect(
&m_requestHandler,
&LLMCore::RequestHandler::completionReceived,
this,
&LLMClientInterface::sendCompletionToClient);
// TODO handle error
// connect(
// &m_requestHandler,
// &LLMCore::RequestHandler::requestFinished,
// this,
// [this](const QString &, bool success, const QString &errorString) {
// if (!success) {
// emit error(errorString);
// }
// });
}
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{
return "Qode Assist";
return "QodeAssist";
}
void LLMClientInterface::startImpl()
@ -170,8 +181,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
auto updatedContext = prepareContext(request, documentInfo);
bool isPreset1Active
= Context::ContextManager::isSpecifyCompletion(documentInfo, m_generalSettings);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
@ -231,6 +241,19 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
@ -277,13 +300,17 @@ 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)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active
= Context::ContextManager::isSpecifyCompletion(documentInfo, m_generalSettings);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
@ -311,7 +338,9 @@ void LLMClientInterface::sendCompletionToClient(
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range;
range["start"] = position;
range["end"] = position;
QJsonObject end = position;
end["character"] = position["character"].toInt() + processedCompletion.length();
range["end"] = end;
completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem);
@ -332,6 +361,4 @@ void LLMClientInterface::sendCompletionToClient(
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::parseCurrentMessage() {}
} // namespace QodeAssist

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -22,6 +22,7 @@
#include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp>
@ -61,9 +62,10 @@ public:
// exposed for tests
void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const;
protected:
void startImpl() override;
void parseCurrentMessage() override;
private:
void handleInitialize(const QJsonObject &request);
@ -84,6 +86,7 @@ private:
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer;
Context::ContextManager *m_contextManager;
};
} // namespace QodeAssist

View File

@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -29,6 +29,36 @@
namespace QodeAssist {
QString mergeWithRightText(const QString &suggestion, const QString &rightText)
{
if (suggestion.isEmpty() || rightText.isEmpty()) {
return suggestion;
}
int j = 0;
QString processed = rightText;
QSet<int> matchedPositions;
for (int i = 0; i < suggestion.length() && j < processed.length(); ++i) {
if (suggestion[i] == processed[j]) {
matchedPositions.insert(j);
++j;
}
}
if (matchedPositions.isEmpty()) {
return suggestion + rightText;
}
QList<int> positions = matchedPositions.values();
std::sort(positions.begin(), positions.end(), std::greater<int>());
for (int pos : positions) {
processed.remove(pos, 1);
}
return suggestion;
}
LLMSuggestion::LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
@ -38,21 +68,28 @@ LLMSuggestion::LLMSuggestion(
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
int endPos = data.range.end.toPositionInDocument(sourceDocument);
startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
startPos = qBound(0, startPos, sourceDocument->characterCount());
endPos = qBound(startPos, endPos, sourceDocument->characterCount());
QTextCursor cursor(sourceDocument);
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
QTextBlock block = cursor.block();
QString blockText = block.text();
int startPosInBlock = startPos - block.position();
int endPosInBlock = endPos - block.position();
int cursorPositionInBlock = cursor.positionInBlock();
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
replacementDocument()->setPlainText(blockText);
QString rightText = blockText.mid(cursorPositionInBlock);
if (!data.text.contains('\n')) {
QString processedRightText = mergeWithRightText(data.text, rightText);
processedRightText = processedRightText.mid(data.text.length());
QString displayText = blockText.left(cursorPositionInBlock) + data.text
+ processedRightText;
replacementDocument()->setPlainText(displayText);
} else {
QString displayText = blockText.left(cursorPositionInBlock) + data.text;
replacementDocument()->setPlainText(displayText);
}
}
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
@ -77,31 +114,82 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
if (next == -1)
return apply();
if (next == -1) {
if (part == Line) {
next = text.length();
} else {
return apply();
}
}
if (part == Line)
++next;
QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty())
if (subText.isEmpty()) {
return false;
}
currentCursor.insertText(subText);
QTextBlock currentBlock = currentCursor.block();
QString textAfterCursor = currentBlock.text().mid(currentCursor.positionInBlock());
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) {
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
const Utils::Text::Position
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
if (!subText.contains('\n')) {
QTextCursor deleteCursor = currentCursor;
deleteCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
deleteCursor.removeSelectedText();
QString mergedText = mergeWithRightText(subText, textAfterCursor);
currentCursor.insertText(mergedText);
} else {
currentCursor.insertText(subText);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) {
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
const Utils::Text::Position newEnd{newStart.line, int(newCompletionText.length())};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
}
}
}
return false;
}
bool LLMSuggestion::apply()
{
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
const QString text = suggestions()[currentSuggestion()].text;
QTextBlock currentBlock = cursor.block();
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QTextCursor editCursor = cursor;
int firstLineEnd = text.indexOf('\n');
if (firstLineEnd != -1) {
QString firstLine = text.left(firstLineEnd);
QString restOfText = text.mid(firstLineEnd);
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
QString mergedFirstLine = mergeWithRightText(firstLine, textAfterCursor);
editCursor.insertText(mergedFirstLine + restOfText);
} else {
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
QString mergedText = mergeWithRightText(text, textAfterCursor);
editCursor.insertText(mergedText);
}
return true;
}
} // namespace QodeAssist

View File

@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -40,5 +40,6 @@ public:
bool applyWord(TextEditor::TextEditorWidget *widget) override;
bool applyLine(TextEditor::TextEditorWidget *widget) override;
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
bool apply() override;
};
} // namespace QodeAssist

View File

@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,13 +1,13 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.5.2",
"Version" : "0.5.12",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : "GPLv3",
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
"Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
${IDE_PLUGIN_DEPENDENCIES}
}

View File

@ -2,5 +2,11 @@
<qresource prefix="/">
<file>resources/images/qoderassist-icon@2x.png</file>
<file>resources/images/qoderassist-icon.png</file>
<file>resources/images/repeat-last-instruct-icon@2x.png</file>
<file>resources/images/repeat-last-instruct-icon.png</file>
<file>resources/images/improve-current-code-icon@2x.png</file>
<file>resources/images/improve-current-code-icon.png</file>
<file>resources/images/suggest-new-icon.png</file>
<file>resources/images/suggest-new-icon@2x.png</file>
</qresource>
</RCC>

View File

@ -1,8 +1,8 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of Qode Assist.
* This file is part of QodeAssist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@ -24,8 +24,10 @@
#include "QodeAssistClient.hpp"
#include <QInputDialog>
#include <QTimer>
#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h>
#include <projectexplorer/projectmanager.h>
@ -35,6 +37,7 @@
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include <context/ChangesManager.h>
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol;
using namespace TextEditor;
@ -46,9 +49,10 @@ namespace QodeAssist {
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface)
, m_recentCharCount(0)
{
setName("Qode Assist");
setName("QodeAssist");
LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter);
@ -128,6 +132,13 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
scheduleRequest(widget);
}
});
// auto editors = BaseTextEditor::textEditorsForDocument(document);
// connect(
// editors.first()->editorWidget(),
// &TextEditorWidget::selectionChanged,
// this,
// [this, editors]() { m_chatButtonHandler.showButton(editors.first()->editorWidget()); });
}
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
@ -142,6 +153,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;
@ -151,6 +170,9 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath),
Position(cursor.mainCursor())}};
if (Settings::codeCompletionSettings().showProgressWidget()) {
m_progressHandler.showProgress(editor);
}
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) {
QTC_ASSERT(editor, return);
@ -160,6 +182,35 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
sendMessage(request);
}
void QodeAssistClient::requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
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(
m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted,
this,
&QodeAssistClient::handleRefactoringResult);
}
m_progressHandler.showProgress(editor);
m_refactorHandler->sendRefactorRequest(editor, instructions);
}
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
{
cancelRunningRequest(editor);
@ -237,6 +288,7 @@ void QodeAssistClient::handleCompletions(
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
m_progressHandler.hideProgress();
if (completions.isEmpty())
return;
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
@ -248,6 +300,7 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd())
return;
m_progressHandler.hideProgress();
cancelRequest(it->id());
m_runningRequests.erase(it);
}
@ -289,4 +342,32 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear();
}
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{
if (!result.success) {
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
return;
}
auto editor = BaseTextEditor::currentTextEditor();
if (!editor) {
LOG_MESSAGE("Refactoring failed: No active editor found");
return;
}
auto editorWidget = editor->editorWidget();
QTextCursor cursor = editorWidget->textCursor();
cursor.beginEditBlock();
int startPos = result.insertRange.begin.toPositionInDocument(editorWidget->document());
int endPos = result.insertRange.end.toPositionInDocument(editorWidget->document());
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
cursor.insertText(result.newText);
cursor.endEditBlock();
m_progressHandler.hideProgress();
}
} // namespace QodeAssist

View File

@ -1,8 +1,8 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of Qode Assist.
* This file is part of QodeAssist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@ -26,6 +26,9 @@
#include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp"
#include "QuickRefactorHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
@ -42,6 +45,8 @@ public:
bool canOpenProject(ProjectExplorer::Project *project) override;
void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
private:
void scheduleRequest(TextEditor::TextEditorWidget *editor);
@ -52,6 +57,7 @@ private:
void setupConnections();
void cleanupConnections();
void handleRefactoringResult(const RefactorResult &result);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
@ -60,6 +66,10 @@ private:
QElapsedTimer m_typingTimer;
int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
LLMClientInterface *m_llmClient;
};
} // namespace QodeAssist

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

293
QuickRefactorHandler.cpp Normal file
View File

@ -0,0 +1,293 @@
/*
* 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 "QuickRefactorHandler.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <context/DocumentContextReader.hpp>
#include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp>
#include <llmcore/PromptTemplateManager.hpp>
#include <llmcore/ProvidersManager.hpp>
#include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp>
namespace QodeAssist {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
: QObject(parent)
, m_requestHandler(new LLMCore::RequestHandler(this))
, m_currentEditor(nullptr)
, m_isRefactoringInProgress(false)
, m_contextManager(this)
{
connect(
m_requestHandler,
&LLMCore::RequestHandler::completionReceived,
this,
&QuickRefactorHandler::handleLLMResponse);
connect(
m_requestHandler,
&LLMCore::RequestHandler::requestFinished,
this,
[this](const QString &requestId, bool success, const QString &errorString) {
if (!success && requestId == m_lastRequestId) {
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = errorString;
emit refactoringCompleted(result);
}
});
}
QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
if (m_isRefactoringInProgress) {
cancelRequest();
}
m_currentEditor = editor;
Utils::Text::Range range;
if (editor->textCursor().hasSelection()) {
QTextCursor cursor = editor->textCursor();
int startPos = cursor.selectionStart();
int endPos = cursor.selectionEnd();
QTextBlock startBlock = editor->document()->findBlock(startPos);
int startLine = startBlock.blockNumber() + 1;
int startColumn = startPos - startBlock.position();
QTextBlock endBlock = editor->document()->findBlock(endPos);
int endLine = endBlock.blockNumber() + 1;
int endColumn = endPos - endBlock.position();
Utils::Text::Position startPosition;
startPosition.line = startLine;
startPosition.column = startColumn;
Utils::Text::Position endPosition;
endPosition.line = endLine;
endPosition.column = endColumn;
range = Utils::Text::Range();
range.begin = startPosition;
range.end = endPosition;
} else {
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
QTextBlock block = editor->document()->findBlock(cursorPos);
int line = block.blockNumber() + 1;
int column = cursorPos - block.position();
Utils::Text::Position cursorPosition;
cursorPosition.line = line;
cursorPosition.column = column;
range = Utils::Text::Range();
range.begin = cursorPosition;
range.end = cursorPosition;
}
m_currentRange = range;
prepareAndSendRequest(editor, instructions, range);
}
void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range)
{
auto &settings = Settings::generalSettings();
auto &providerRegistry = LLMCore::ProvidersManager::instance();
auto &promptManager = LLMCore::PromptTemplateManager::instance();
const auto providerName = settings.caProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
RefactorResult result;
result.success = false;
result.errorMessage = QString("No provider found with name: %1").arg(providerName);
emit refactoringCompleted(result);
return;
}
const auto templateName = settings.caTemplate();
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
RefactorResult result;
result.success = false;
result.errorMessage = QString("No template found with name: %1").arg(templateName);
emit refactoringCompleted(result);
return;
}
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", settings.caModel()}, {"stream", Settings::chatAssistantSettings().stream()}};
config.apiKey = provider->apiKey();
LLMCore::ContextData context = prepareContext(editor, range, instructions);
provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QString requestId = QUuid::createUuid().toString();
m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}};
m_isRefactoringInProgress = true;
m_requestHandler->sendLLMRequest(config, request);
}
LLMCore::ContextData QuickRefactorHandler::prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions)
{
LLMCore::ContextData context;
auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader;
auto documentInfo = documentReader.readDocument(textDocument->filePath().toUrlishString());
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available");
return context;
}
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
// TODO add selecting content before and after cursor/selection
QString fullContent = documentInfo.document->toPlainText();
QString taggedContent = fullContent;
if (cursor.hasSelection()) {
int selEnd = cursor.selectionEnd();
int selStart = cursor.selectionStart();
taggedContent
.insert(selEnd, selEnd == cursorPos ? "<selection_end><cursor>" : "<selection_end>");
taggedContent.insert(
selStart, selStart == cursorPos ? "<cursor><selection_start>" : "<selection_start>");
} else {
taggedContent.insert(cursorPos, "<cursor>");
}
QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt();
systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath;
systemPrompt += "\n\nCode context with position markers:";
systemPrompt += taggedContent;
systemPrompt += "\n\nOutput format:";
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
"between<selection_start><selection_end> or be "
"inserted at cursor position<cursor>";
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
"code block markers";
systemPrompt += "\n- The output should be ready to insert directly into the editor";
systemPrompt += "\n- Follow the existing code style and indentation patterns";
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
}
context.systemPrompt = systemPrompt;
QVector<LLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
}
void QuickRefactorHandler::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
if (request["id"].toString() != m_lastRequestId) {
return;
}
if (isComplete) {
QString cleanedResponse = response.trimmed();
if (cleanedResponse.startsWith("```")) {
int firstNewLine = cleanedResponse.indexOf('\n');
int lastFence = cleanedResponse.lastIndexOf("```");
if (firstNewLine != -1 && lastFence > firstNewLine) {
cleanedResponse
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
} else if (lastFence != -1) {
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
}
}
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
}
}
void QuickRefactorHandler::cancelRequest()
{
if (m_isRefactoringInProgress) {
m_requestHandler->cancelRequest(m_lastRequestId);
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result);
}
}
} // namespace QodeAssist

77
QuickRefactorHandler.hpp Normal file
View File

@ -0,0 +1,77 @@
/*
* 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 <QJsonObject>
#include <QObject>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <llmcore/RequestHandler.hpp>
namespace QodeAssist {
struct RefactorResult
{
QString newText;
Utils::Text::Range insertRange;
bool success;
QString errorMessage;
};
class QuickRefactorHandler : public QObject
{
Q_OBJECT
public:
explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override;
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest();
signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result);
private:
void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
LLMCore::ContextData prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions);
LLMCore::RequestHandler *m_requestHandler;
TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress;
QString m_lastRequestId;
Context::ContextManager m_contextManager;
};
} // namespace QodeAssist

View File

@ -2,7 +2,7 @@
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
![Static Badge](https://img.shields.io/badge/QtCreator-15.0.1-brightgreen)
![Static Badge](https://img.shields.io/badge/QtCreator-16.0.1-brightgreen)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
@ -13,15 +13,6 @@
> - The QodeAssist developer bears no responsibility for any charges incurred
> - Please carefully review the provider's pricing and your account settings before use
⚠️ **Commercial Support and Custom Development**
> The QodeAssist developer offers commercial services for:
> - Adapting the plugin for specific Qt Creator versions
> - Custom development for particular operating systems
> - Integration with specific language models
> - Implementing custom features and modifications
>
> For commercial inquiries, please contact: qodeassist.dev@pm.me
## Table of Contents
1. [Overview](#overview)
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
@ -36,6 +27,7 @@
11. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
12. [Development Progress](#development-progress)
13. [Hotkeys](#hotkeys)
14. [Ignoring Files](#ignoring-files)
14. [Troubleshooting](#troubleshooting)
15. [Support the Development](#support-the-development-of-qodeassist)
16. [How to Build](#how-to-build)
@ -43,6 +35,8 @@
## Overview
- AI-powered code completion
- Sharing IDE opened files with model context (disabled by default, need enable in settings)
- Quick refactor code via fast chat command and opened files
- Chat functionality:
- Side and Bottom panels
- Chat history autosave and restore
@ -60,7 +54,6 @@
- Google AI
- OpenAI-compatible providers(eg. llama.cpp, https://openrouter.ai)
- Extensive library of model-specific templates
- Custom template support
- Easy configuration and model selection
Join our Discord Community: Have questions or want to discuss QodeAssist? Join our [Discord server](https://discord.gg/BGMkUsXUgf) to connect with other users and get support!
@ -70,6 +63,11 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
</details>
<details>
<summary>Quick refactor in code: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details>
<details>
<summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
@ -94,6 +92,8 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
1. Install Latest Qt Creator
2. Download the QodeAssist plugin for your Qt Creator
- Remove old version plugin if already was installed
- on macOS for QtCreator 16: ~/Library/Application Support/QtProject/Qt Creator/plugins/16.0.0/petrmironychev.qodeassist
- on windows for QtCreator 16: C:\Users\<user>\AppData\Local\QtProject\qtcreator\plugins\16.0.0\petrmironychev.qodeassist\lib\qtcreator\plugins
3. Launch Qt Creator and install the plugin:
- Go to:
- MacOS: Qt Creator -> About Plugins...
@ -172,7 +172,7 @@ ollama run qwen2.5-coder:32b
```
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
2. Navigate to the "Qode Assist" tab
2. Navigate to the "QodeAssist" tab
3. On the "General" page, verify:
- Ollama is selected as your LLM provider
- The URL is set to http://localhost:11434
@ -237,7 +237,9 @@ Linked files provide persistent context throughout the conversation:
## QtCreator Version Compatibility
- QtCreator 15.0.1 - 0.4.8 - 0.5.x
- QtCreator 16.0.1 - 0.5.7 - 0.x.x
- QtCreator 16.0.0 - 0.5.2 - 0.5.6
- QtCreator 15.0.1 - 0.4.8 - 0.5.1
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
- QtCreator 14.0.1 - 0.2.2 plugin version and below
@ -256,8 +258,49 @@ Linked files provide persistent context throughout the conversation:
- To call manual request to suggestion, you can use or change it in settings
- on Mac: Option + Command + Q
- on Windows: Ctrl + Alt + Q
- on Linux with KDE Plasma: Ctrl + Alt + Q
- To insert the full suggestion, you can use the TAB key
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
- To call Quick Refactor dialog, select some code or place cursor and press
- on Mac: Option + Command + R
- on Windows: Ctrl + Alt + R
- on Linux with KDE Plasma: Ctrl + Alt + R
## Ignoring Files
QodeAssist supports the ability to ignore files in context using a .qodeassistignore file. This allows you to exclude specific files from the context during code completion and in the chat assistant, which is especially useful for large projects.
### How to Use .qodeassistignore
- Create a .qodeassistignore file in the root directory of your project near CMakeLists.txt or pro.
- Add patterns for files and directories that should be excluded from the context.
- QodeAssist will automatically detect this file and apply the exclusion rules.
### .qodeassistignore File Format
The file format is similar to .gitignore:
- Each pattern is written on a separate line
- Empty lines are ignored
- Lines starting with # are considered comments
- Standard wildcards work the same as in .gitignore
- To negate a pattern, use ! at the beginning of the line
```
# Ignore all files in the build directory
build/
# Ignore all temporary files
*.tmp
*.temp
# Ignore all files with .log extension
*.log
# Ignore a specific file
src/generated/autogen.cpp
# Ignore nested directories
**/node_modules/
# Negation - DO NOT ignore this file
!src/important.cpp
```
## Troubleshooting
@ -278,7 +321,7 @@ If you need compatiblity with another os, you have to build manualy. our experim
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
1. Open Qt Creator settings
2. Navigate to the "Qode Assist" tab
2. Navigate to the "QodeAssist" tab
3. Pick settings page for reset
4. Click on the "Reset Page to Defaults" button
- The API key will not reset

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -52,10 +52,10 @@ void UpdateStatusWidget::setDefaultAction(QAction *action)
void UpdateStatusWidget::showUpdateAvailable(const QString &version)
{
m_versionLabel->setText(tr("new version: v%1").arg(version));
m_versionLabel->setText(tr("New version: v%1").arg(version));
m_versionLabel->setVisible(true);
m_updateButton->setVisible(true);
m_updateButton->setToolTip(tr("Update QodeAssist to version %1").arg(version));
m_updateButton->setToolTip(tr("Check update information"));
}
void UpdateStatusWidget::hideUpdateInfo()

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -7,6 +7,8 @@ add_library(Context STATIC
IDocumentReader.hpp
TokenUtils.hpp TokenUtils.cpp
ProgrammingLanguage.hpp ProgrammingLanguage.cpp
IContextManager.hpp
IgnoreManager.hpp IgnoreManager.cpp
)
target_link_libraries(Context

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -24,19 +24,20 @@
#include <QJsonObject>
#include <QTextStream>
#include "GeneralSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>
#include <texteditor/textdocument.h>
#include "Logger.hpp"
namespace QodeAssist::Context {
ContextManager &ContextManager::instance()
{
static ContextManager manager;
return manager;
}
ContextManager::ContextManager(QObject *parent)
: QObject(parent)
, m_ignoreManager(new IgnoreManager(this))
{}
QString ContextManager::readFile(const QString &filePath) const
@ -53,12 +54,40 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
{
QList<ContentFile> 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);
}
return files;
}
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
{
QStringList sourceFiles;
if (!project)
return sourceFiles;
auto projectNode = project->rootProjectNode();
if (!projectNode)
return sourceFiles;
projectNode->forEachNode(
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) {
sourceFiles.append(fileNode->filePath().toUrlishString());
}
},
nullptr);
return sourceFiles;
}
ContentFile ContextManager::createContentFile(const QString &filePath) const
{
ContentFile contentFile;
@ -68,7 +97,7 @@ ContentFile ContextManager::createContentFile(const QString &filePath) const
return contentFile;
}
ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &documentInfo)
ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &documentInfo) const
{
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available for" + documentInfo.filePath);
@ -78,9 +107,10 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
return Context::ProgrammingLanguageUtils::fromMimeType(documentInfo.mimeType);
}
bool ContextManager::isSpecifyCompletion(
const DocumentInfo &documentInfo, const Settings::GeneralSettings &generalSettings)
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
{
const auto &generalSettings = Settings::generalSettings();
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
@ -88,4 +118,68 @@ bool ContextManager::isSpecifyCompletion(
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
}
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const
{
auto documents = Core::DocumentModel::openedDocuments();
QList<QPair<QString, QString>> files;
for (const auto *document : std::as_const(documents)) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
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()});
}
}
return files;
}
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
{
QString context = "User files context:\n";
auto documents = Core::DocumentModel::openedDocuments();
for (const auto *document : documents) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
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();
context += "\n";
}
return context;
}
IgnoreManager *ContextManager::ignoreManager() const
{
return m_ignoreManager;
}
} // namespace QodeAssist::Context

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -23,31 +23,38 @@
#include <QString>
#include "ContentFile.hpp"
#include "IDocumentReader.hpp"
#include "IContextManager.hpp"
#include "IgnoreManager.hpp"
#include "ProgrammingLanguage.hpp"
#include "settings/GeneralSettings.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class ContextManager : public QObject
class ContextManager : public QObject, public IContextManager
{
Q_OBJECT
public:
static ContextManager &instance();
QString readFile(const QString &filePath) const;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const;
explicit ContextManager(QObject *parent = nullptr);
~ContextManager() override = default;
static ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo);
static bool isSpecifyCompletion(
const DocumentInfo &documentInfo, const Settings::GeneralSettings &generalSettings);
QString readFile(const QString &filePath) const override;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
ContentFile createContentFile(const QString &filePath) const override;
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
IgnoreManager *ignoreManager() const;
private:
explicit ContextManager(QObject *parent = nullptr);
~ContextManager() = default;
ContextManager(const ContextManager &) = delete;
ContextManager &operator=(const ContextManager &) = delete;
ContentFile createContentFile(const QString &filePath) const;
IgnoreManager *m_ignoreManager;
};
} // namespace QodeAssist::Context

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -145,9 +145,24 @@ CopyrightInfo DocumentContextReader::findCopyright()
QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured().toLower();
if (matchedText.contains("copyright") || matchedText.contains("(C)")
|| matchedText.contains("(c)") || matchedText.contains("©")
|| getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) {
bool hasCopyrightIndicator = matchedText.contains("copyright")
|| matchedText.contains("(c)") || matchedText.contains("©")
|| matchedText.contains("copr.")
|| matchedText.contains("all rights reserved")
|| matchedText.contains("proprietary")
|| matchedText.contains("licensed under")
|| matchedText.contains("license:")
|| matchedText.contains("gpl") || matchedText.contains("lgpl")
|| matchedText.contains("mit license")
|| matchedText.contains("apache license")
|| matchedText.contains("bsd license")
|| matchedText.contains("mozilla public license")
|| matchedText.contains("copyleft");
bool hasYear = getYearRegex().match(matchedText).hasMatch();
bool hasName = getNameRegex().match(matchedText).hasMatch();
if ((hasCopyrightIndicator && (hasYear || hasName)) || (hasYear && hasName)) {
int startPos = match.capturedStart();
int endPos = match.capturedEnd();

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -0,0 +1,49 @@
/*
* 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 <QObject>
#include <QString>
#include "ContentFile.hpp"
#include "IDocumentReader.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class IContextManager
{
public:
virtual ~IContextManager() = default;
virtual QString readFile(const QString &filePath) const = 0;
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
virtual QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const = 0;
virtual ContentFile createContentFile(const QString &filePath) const = 0;
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;
virtual bool isSpecifyCompletion(const DocumentInfo &documentInfo) const = 0;
};
} // namespace QodeAssist::Context

274
context/IgnoreManager.cpp Normal file
View File

@ -0,0 +1,274 @@
/*
* 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 "IgnoreManager.hpp"
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QTextStream>
#include "logger/Logger.hpp"
namespace QodeAssist::Context {
IgnoreManager::IgnoreManager(QObject *parent)
: QObject(parent)
{
auto projectManager = ProjectExplorer::ProjectManager::instance();
if (projectManager) {
connect(
projectManager,
&ProjectExplorer::ProjectManager::projectRemoved,
this,
&IgnoreManager::removeIgnorePatterns);
}
connect(
QCoreApplication::instance(),
&QCoreApplication::aboutToQuit,
this,
&IgnoreManager::cleanupConnections);
}
IgnoreManager::~IgnoreManager()
{
cleanupConnections();
}
void IgnoreManager::cleanupConnections()
{
QList<ProjectExplorer::Project *> projects = m_projectConnections.keys();
for (ProjectExplorer::Project *project : projects) {
if (project) {
disconnect(m_projectConnections.take(project));
}
}
m_projectConnections.clear();
m_projectIgnorePatterns.clear();
m_ignoreCache.clear();
}
bool IgnoreManager::shouldIgnore(const QString &filePath, ProjectExplorer::Project *project) const
{
if (!project)
return false;
if (!m_projectIgnorePatterns.contains(project)) {
const_cast<IgnoreManager *>(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);
}
bool IgnoreManager::matchesIgnorePatterns(const QString &path, const QStringList &patterns) const
{
QString cacheKey = path + ":" + patterns.join("|");
if (m_ignoreCache.contains(cacheKey))
return m_ignoreCache[cacheKey];
bool result = isPathExcluded(path, patterns);
m_ignoreCache.insert(cacheKey, result);
return result;
}
bool IgnoreManager::isPathExcluded(const QString &path, const QStringList &patterns) const
{
bool excluded = false;
for (const QString &pattern : patterns) {
if (pattern.isEmpty() || pattern.startsWith('#'))
continue;
bool isNegative = pattern.startsWith('!');
QString actualPattern = isNegative ? pattern.mid(1) : pattern;
bool matches = matchPathWithPattern(path, actualPattern);
if (matches) {
excluded = !isNegative;
}
}
return excluded;
}
bool IgnoreManager::matchPathWithPattern(const QString &path, const QString &pattern) const
{
QString adjustedPattern = pattern.trimmed();
bool matchFromRoot = adjustedPattern.startsWith('/');
if (matchFromRoot)
adjustedPattern = adjustedPattern.mid(1);
bool matchDirOnly = adjustedPattern.endsWith('/');
if (matchDirOnly)
adjustedPattern.chop(1);
QString regexPattern = QRegularExpression::escape(adjustedPattern);
regexPattern.replace("\\*\\*", ".*");
regexPattern.replace("\\*", "[^/]*");
regexPattern.replace("\\?", ".");
if (matchFromRoot)
regexPattern = QString("^%1").arg(regexPattern);
else
regexPattern = QString("(^|/)%1").arg(regexPattern);
if (matchDirOnly)
regexPattern = QString("%1$").arg(regexPattern);
else
regexPattern = QString("%1($|/)").arg(regexPattern);
QRegularExpression regex(regexPattern);
QRegularExpressionMatch match = regex.match(path);
return match.hasMatch();
}
QStringList IgnoreManager::loadIgnorePatterns(ProjectExplorer::Project *project)
{
QStringList patterns;
if (!project)
return patterns;
QString ignoreFile = ignoreFilePath(project);
if (ignoreFile.isEmpty() || !QFile::exists(ignoreFile)) {
// LOG_MESSAGE(
// QString("No .qodeassistignore file found for project: %1").arg(project->displayName()));
return patterns;
}
QFile file(ignoreFile);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
LOG_MESSAGE(QString("Could not open .qodeassistignore file: %1").arg(ignoreFile));
return patterns;
}
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (!line.isEmpty() && !line.startsWith('#'))
patterns << line;
}
LOG_MESSAGE(QString("Successfully loaded .qodeassistignore file: %1 with %2 patterns")
.arg(ignoreFile)
.arg(patterns.size()));
return patterns;
}
void IgnoreManager::reloadIgnorePatterns(ProjectExplorer::Project *project)
{
if (!project)
return;
QStringList patterns = loadIgnorePatterns(project);
m_projectIgnorePatterns[project] = patterns;
QStringList keysToRemove;
QString projectPath = project->projectDirectory().toUrlishString();
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(projectPath))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
if (!m_projectConnections.contains(project)) {
QPointer<ProjectExplorer::Project> projectPtr(project);
auto connection = connect(project, &QObject::destroyed, this, [this, projectPtr]() {
if (projectPtr) {
m_projectIgnorePatterns.remove(projectPtr);
m_projectConnections.remove(projectPtr);
QStringList keysToRemove;
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(projectPtr->projectDirectory().toUrlishString()))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
}
});
m_projectConnections[project] = connection;
}
}
void IgnoreManager::removeIgnorePatterns(ProjectExplorer::Project *project)
{
m_projectIgnorePatterns.remove(project);
QStringList keysToRemove;
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(project->projectDirectory().toUrlishString()))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
if (m_projectConnections.contains(project)) {
disconnect(m_projectConnections[project]);
m_projectConnections.remove(project);
}
LOG_MESSAGE(QString("Removed ignore patterns for project: %1").arg(project->displayName()));
}
void IgnoreManager::reloadAllPatterns()
{
QList<ProjectExplorer::Project *> projects = m_projectIgnorePatterns.keys();
for (ProjectExplorer::Project *project : projects) {
if (project) {
reloadIgnorePatterns(project);
}
}
m_ignoreCache.clear();
}
QString IgnoreManager::ignoreFilePath(ProjectExplorer::Project *project) const
{
if (!project) {
return QString();
}
return project->projectDirectory().toUrlishString() + "/.qodeassistignore";
}
} // namespace QodeAssist::Context

62
context/IgnoreManager.hpp Normal file
View 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 <QHash>
#include <QObject>
#include <QPointer>
#include <QStringList>
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);
void removeIgnorePatterns(ProjectExplorer::Project *project);
void reloadAllPatterns();
private slots:
void cleanupConnections();
private:
bool matchesIgnorePatterns(const QString &path, const QStringList &patterns) const;
bool isPathExcluded(const QString &path, const QStringList &patterns) const;
bool matchPathWithPattern(const QString &path, const QString &pattern) const;
QStringList loadIgnorePatterns(ProjectExplorer::Project *project);
QString ignoreFilePath(ProjectExplorer::Project *project) const;
QHash<ProjectExplorer::Project *, QStringList> m_projectIgnorePatterns;
mutable QHash<QString, bool> m_ignoreCache;
QHash<ProjectExplorer::Project *, QMetaObject::Connection> m_projectConnections;
};
} // namespace QodeAssist::Context

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -34,6 +34,13 @@ struct Message
// clang-format on
};
struct FileMetadata
{
QString filePath;
QString content;
bool operator==(const FileMetadata &) const = default;
};
struct ContextData
{
std::optional<QString> systemPrompt;
@ -41,6 +48,7 @@ struct ContextData
std::optional<QString> suffix;
std::optional<QString> fileContext;
std::optional<QVector<Message>> history;
std::optional<QList<FileMetadata>> filesMetadata;
bool operator==(const ContextData &) const = default;
};

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -22,15 +22,51 @@
#include <QJsonDocument>
#include <QNetworkReply>
#include <QThread>
namespace QodeAssist::LLMCore {
RequestHandler::RequestHandler(QObject *parent)
: RequestHandlerBase(parent)
, m_manager(new QNetworkAccessManager(this))
{}
{
connect(
this,
&RequestHandler::doSendRequest,
this,
&RequestHandler::sendLLMRequestInternal,
Qt::QueuedConnection);
connect(
this,
&RequestHandler::doCancelRequest,
this,
&RequestHandler::cancelRequestInternal,
Qt::QueuedConnection);
}
RequestHandler::~RequestHandler()
{
for (auto reply : m_activeRequests) {
reply->abort();
reply->deleteLater();
}
m_activeRequests.clear();
m_accumulatedResponses.clear();
}
void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request)
{
emit doSendRequest(config, request);
}
bool RequestHandler::cancelRequest(const QString &id)
{
emit doCancelRequest(id);
return true;
}
void RequestHandler::sendLLMRequestInternal(const LLMConfig &config, const QJsonObject &request)
{
LOG_MESSAGE(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
.arg(
@ -39,6 +75,8 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
QNetworkRequest networkRequest(config.url);
networkRequest.setTransferTimeout(300000);
config.provider->prepareNetworkRequest(networkRequest);
QNetworkReply *reply
@ -55,20 +93,28 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
handleLLMResponse(reply, request, config);
});
connect(reply, &QNetworkReply::finished, this, [this, reply, requestId]() {
reply->deleteLater();
m_activeRequests.remove(requestId);
if (reply->error() != QNetworkReply::NoError) {
LOG_MESSAGE(QString("Error details: %1\nStatus code: %2\nResponse: %3")
.arg(reply->errorString())
.arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())
.arg(QString(reply->readAll())));
emit requestFinished(requestId, false, reply->errorString());
} else {
LOG_MESSAGE("Request finished successfully");
emit requestFinished(requestId, true, QString());
}
});
connect(
reply,
&QNetworkReply::finished,
this,
[this, reply, requestId]() {
m_activeRequests.remove(requestId);
if (reply->error() != QNetworkReply::NoError) {
QString errorMessage = reply->errorString();
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
LOG_MESSAGE(
QString("Error details: %1\nStatus code: %2").arg(errorMessage).arg(statusCode));
emit requestFinished(requestId, false, errorMessage);
} else {
LOG_MESSAGE("Request finished successfully");
emit requestFinished(requestId, true, QString());
}
reply->deleteLater();
},
Qt::QueuedConnection);
}
void RequestHandler::handleLLMResponse(
@ -98,17 +144,18 @@ void RequestHandler::handleLLMResponse(
m_accumulatedResponses.remove(reply);
}
bool RequestHandler::cancelRequest(const QString &id)
void RequestHandler::cancelRequestInternal(const QString &id)
{
QMutexLocker locker(&m_mutex);
if (m_activeRequests.contains(id)) {
QNetworkReply *reply = m_activeRequests[id];
reply->abort();
m_activeRequests.remove(id);
m_accumulatedResponses.remove(reply);
locker.unlock();
emit requestCancelled(id);
return true;
}
return false;
}
bool RequestHandler::processSingleLineCompletion(

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -20,6 +20,7 @@
#pragma once
#include <QJsonObject>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QObject>
@ -32,17 +33,32 @@ namespace QodeAssist::LLMCore {
class RequestHandler : public RequestHandlerBase
{
Q_OBJECT
public:
explicit RequestHandler(QObject *parent = nullptr);
~RequestHandler() override;
void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) override;
bool cancelRequest(const QString &id) override;
void handleLLMResponse(QNetworkReply *reply, const QJsonObject &request, const LLMConfig &config);
signals:
void doSendRequest(QodeAssist::LLMCore::LLMConfig config, QJsonObject request);
void doCancelRequest(QString id);
private slots:
void sendLLMRequestInternal(
const QodeAssist::LLMCore::LLMConfig &config, const QJsonObject &request);
void cancelRequestInternal(const QString &id);
void handleLLMResponse(
QNetworkReply *reply,
const QJsonObject &request,
const QodeAssist::LLMCore::LLMConfig &config);
private:
QNetworkAccessManager *m_manager;
QMap<QString, QNetworkReply *> m_activeRequests;
QMap<QNetworkReply *, QString> m_accumulatedResponses;
QNetworkAccessManager *m_manager;
QMutex m_mutex;
bool processSingleLineCompletion(
QNetworkReply *reply,

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -28,7 +28,7 @@ void Logger::log(const QString &message, bool silent)
if (!m_loggingEnabled)
return;
const QString prefixedMessage = QLatin1String("[Qode Assist] ") + message;
const QString prefixedMessage = QLatin1String("[QodeAssist] ") + message;
if (silent) {
Core::MessageManager::writeSilently(prefixedMessage);
} else {
@ -43,7 +43,7 @@ void Logger::logMessages(const QStringList &messages, bool silent)
QStringList prefixedMessages;
for (const QString &message : messages) {
prefixedMessages << (QLatin1String("[Qode Assist] ") + message);
prefixedMessages << (QLatin1String("[QodeAssist] ") + message);
}
if (silent) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -0,0 +1,46 @@
/*
* 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 "CodestralProvider.hpp"
#include "settings/ProviderSettings.hpp"
namespace QodeAssist::Providers {
QString CodestralProvider::name() const
{
return "Codestral";
}
QString CodestralProvider::url() const
{
return "https://codestral.mistral.ai";
}
bool CodestralProvider::supportsModelListing() const
{
return false;
}
QString CodestralProvider::apiKey() const
{
return Settings::providerSettings().codestralApiKey();
}
} // namespace QodeAssist::Providers

View File

@ -0,0 +1,35 @@
/*
* 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 "MistralAIProvider.hpp"
namespace QodeAssist::Providers {
class CodestralProvider : public MistralAIProvider
{
public:
QString name() const override;
QString url() const override;
bool supportsModelListing() const override;
QString apiKey() const override;
};
} // namespace QodeAssist::Providers

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -30,7 +30,6 @@
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
namespace QodeAssist::Providers {

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -167,6 +167,7 @@ QList<QString> LlamaCppProvider::validateRequest(
{"model", {}},
{"input_prefix", {}},
{"input_suffix", {}},
{"input_extra", {}},
{"prompt", {}},
{"temperature", {}},
{"top_p", {}},

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -30,6 +30,7 @@
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/ProviderSettings.hpp"
namespace QodeAssist::Providers {
@ -139,6 +140,7 @@ QList<QString> OllamaProvider::getInstalledModels(const QString &url)
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1%2").arg(url, "/api/tags"));
prepareNetworkRequest(request);
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
@ -210,6 +212,10 @@ QString OllamaProvider::apiKey() const
void OllamaProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
const auto key = Settings::providerSettings().ollamaBasicAuthApiKey();
if (!key.isEmpty()) {
networkRequest.setRawHeader("Authorization", "Basic " + key.toLatin1());
}
}
LLMCore::ProviderID OllamaProvider::providerID() const

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*

Some files were not shown because too many files have changed in this diff Show More