Compare commits

..

34 Commits

Author SHA1 Message Date
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
52 changed files with 2104 additions and 55 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.6",
"version": "0.5.11",
"status": "draft",
"is_pack": false,
"released_at": null,
@ -45,8 +45,33 @@
},
{
"version": "0.5.6",
"is_latest": true,
"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": true,
"released_at": "2025-04-24T21: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
@ -103,6 +104,10 @@ add_qtc_plugin(QodeAssist
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)

View File

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

View File

@ -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

@ -73,6 +73,8 @@ public:
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index);
signals:
void tokensThresholdChanged();
void modelReseted();

View File

@ -510,7 +510,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 +537,19 @@ 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;
}
} // namespace QodeAssist::Chat

View File

@ -78,6 +78,7 @@ public:
QString chatFileName() const;
void setRecentFilePath(const QString &filePath);
bool shouldIgnoreFileForAttach(const Utils::FilePath &filePath);
public slots:
void sendMessage(const QString &message);

View File

@ -28,12 +28,19 @@ Rectangle {
property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model
property bool isUserMessage: false
property int messageIndex: -1
signal resetChatToMessage(int index)
height: msgColumn.implicitHeight + 10
radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
HoverHandler {
id: mouse
}
ColumnLayout {
id: msgColumn
@ -128,6 +135,21 @@ 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

View File

@ -76,6 +76,10 @@ ChatRootView {
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
}
openChatHistory.onClicked: root.openChatHistoryFolder()
expandScrollbar {
text: scroll.isPreviewMode ? "»" : "«"
onClicked: scroll.isPreviewMode = !scroll.isPreviewMode
}
}
ListView {
@ -92,11 +96,19 @@ 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
onResetChatToMessage: function(index) {
messageInput.text = model.content
messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(index)
}
}
header: Item {
@ -106,6 +118,50 @@ ChatRootView {
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
property bool isPreviewMode: false
readonly property int previewWidth: 30
implicitWidth: isPreviewMode ? scroll.previewWidth : 16
contentItem: Rectangle {
implicitWidth: scroll.isPreviewMode ? scroll.previewWidth : 6
implicitHeight: 100
radius: 3
color: scroll.pressed ? palette.dark :
scroll.hovered ? palette.mid :
palette.button
Behavior on implicitWidth {
NumberAnimation { duration: 150 }
}
}
background: Rectangle {
color: scroll.isPreviewMode ? "transparent" :
palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
radius: 3
}
ChatPreviewBar {
anchors.fill: parent
targetView: chatListView
visible: parent.isPreviewMode
opacity: parent.isPreviewMode ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
}
onCountChanged: {

View File

@ -25,7 +25,7 @@ TextEdit {
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
textFormat: Text.StyledText
textFormat: Text.MarkdownText
selectionColor: palette.highlight
color: palette.text
}

View File

@ -0,0 +1,135 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Controls
Rectangle {
id: root
property ListView targetView: null
property int previewWidth: 50
property color userMessageColor: "#92BD6C"
property color assistantMessageColor: palette.button
width: previewWidth
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
Behavior on opacity {
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
Column {
id: previewContainer
anchors.fill: parent
anchors.margins: 2
spacing: 2
Repeater {
model: targetView ? targetView.model : null
Rectangle {
required property int index
required property var model
width: parent.width
height: {
if (!targetView || !targetView.count) return 0
const availableHeight = root.height - ((targetView.count - 1) * previewContainer.spacing)
return availableHeight / targetView.count
}
radius: 4
color: model.roleType === ChatModel.User ?
userMessageColor :
assistantMessageColor
opacity: root.opacity
transform: Translate {
x: root.opacity * 50 - 50
}
Behavior on transform {
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (targetView) {
targetView.positionViewAtIndex(index, ListView.Center)
}
}
HoverHandler {
id: hover
}
}
Rectangle {
anchors.fill: parent
color: palette.highlight
opacity: hover.hovered ? 0.2 : 0
radius: parent.radius
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Rectangle {
anchors.fill: parent
color: palette.highlight
opacity: {
if (!targetView) return 0
const viewY = targetView.contentY
const viewHeight = targetView.height
const totalHeight = targetView.contentHeight
const itemPosition = index / targetView.count * totalHeight
const itemHeight = totalHeight / targetView.count
return (itemPosition + itemHeight > viewY &&
itemPosition < viewY + viewHeight) ? 0.2 : 0
}
radius: parent.radius
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
ToolTip.visible: hover.hovered
ToolTip.text: {
const maxPreviewLength = 100
return model.content.length > maxPreviewLength ?
model.content.substring(0, maxPreviewLength) + "..." :
model.content
}
}
}
}
}

View File

@ -30,6 +30,7 @@ Rectangle {
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
property alias expandScrollbar: expandScrollbarId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
@ -84,5 +85,12 @@ Rectangle {
Badge {
id: tokensBadgeId
}
QoAButton {
id: expandScrollbarId
width: 16
height: 16
}
}
}

View File

@ -289,6 +289,11 @@ LLMCore::ContextData LLMClientInterface::prepareContext(
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
}
Context::ContextManager *LLMClientInterface::contextManager() const
{
return m_contextManager;
}
void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete)
{
@ -322,7 +327,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);

View File

@ -62,6 +62,8 @@ public:
// exposed for tests
void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const;
protected:
void startImpl() override;

View File

@ -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

@ -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,7 +1,7 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.5.6",
"Version" : "0.5.11",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",

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

@ -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,6 +49,7 @@ namespace QodeAssist {
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface)
, m_recentCharCount(0)
{
setName("QodeAssist");
@ -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;
@ -163,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);
@ -294,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

@ -26,7 +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>
@ -43,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);
@ -53,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;
@ -62,6 +67,9 @@ private:
QElapsedTimer m_typingTimer;
int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
LLMClientInterface *m_llmClient;
};
} // namespace 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-16.0.0-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">
@ -239,7 +237,8 @@ Linked files provide persistent context throughout the conversation:
## QtCreator Version Compatibility
- QtCreator 16.0.0 - 0.5.2 - 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
@ -262,6 +261,46 @@ Linked files provide persistent context throughout the conversation:
- 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

View File

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

View File

@ -27,6 +27,7 @@
#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>
@ -36,6 +37,7 @@ namespace QodeAssist::Context {
ContextManager::ContextManager(QObject *parent)
: QObject(parent)
, m_ignoreManager(new IgnoreManager(this))
{}
QString ContextManager::readFile(const QString &filePath) const
@ -52,6 +54,13 @@ QList<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);
}
@ -121,6 +130,14 @@ QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList exc
continue;
auto filePath = textDocument->filePath().toUrlishString();
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
if (!excludeFiles.contains(filePath)) {
files.append({filePath, textDocument->plainText()});
}
@ -144,6 +161,13 @@ QString ContextManager::openedFilesContext(const QStringList excludeFiles)
if (excludeFiles.contains(filePath))
continue;
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
context += QString("File: %1\n").arg(filePath);
context += textDocument->plainText();
@ -153,4 +177,9 @@ QString ContextManager::openedFilesContext(const QStringList excludeFiles)
return context;
}
IgnoreManager *ContextManager::ignoreManager() const
{
return m_ignoreManager;
}
} // namespace QodeAssist::Context

View File

@ -24,6 +24,7 @@
#include "ContentFile.hpp"
#include "IContextManager.hpp"
#include "IgnoreManager.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
@ -49,6 +50,11 @@ public:
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
IgnoreManager *ignoreManager() const;
private:
IgnoreManager *m_ignoreManager;
};
} // 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

@ -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

@ -21,6 +21,7 @@
#include "llmcore/ProvidersManager.hpp"
#include "providers/ClaudeProvider.hpp"
#include "providers/CodestralProvider.hpp"
#include "providers/GoogleAIProvider.hpp"
#include "providers/LMStudioProvider.hpp"
#include "providers/LlamaCppProvider.hpp"
@ -44,6 +45,7 @@ inline void registerProviders()
providerManager.registerProvider<MistralAIProvider>();
providerManager.registerProvider<GoogleAIProvider>();
providerManager.registerProvider<LlamaCppProvider>();
providerManager.registerProvider<CodestralProvider>();
}
} // namespace QodeAssist::Providers

View File

@ -43,6 +43,7 @@
#include "ConfigurationManager.hpp"
#include "QodeAssistClient.hpp"
#include "UpdateStatusWidget.hpp"
#include "Version.hpp"
#include "chat/ChatOutputPane.h"
#include "chat/NavigationPanel.hpp"
@ -50,13 +51,18 @@
#include "llmcore/PromptProviderFim.hpp"
#include "llmcore/ProvidersManager.hpp"
#include "logger/RequestPerformanceLogger.hpp"
#include "providers/Providers.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettingsPanel.hpp"
#include "settings/SettingsConstants.hpp"
#include "UpdateStatusWidget.hpp"
#include "providers/Providers.hpp"
#include "templates/Templates.hpp"
#include "widgets/QuickRefactorDialog.hpp"
#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <texteditor/texteditorconstants.h>
#include <QInputDialog>
using namespace Utils;
using namespace Core;
@ -134,6 +140,41 @@ public:
if (Settings::generalSettings().enableCheckUpdate()) {
QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates);
}
ActionBuilder quickRefactorAction(this, "QodeAssist.QuickRefactor");
const QKeySequence quickRefactorShortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_R);
quickRefactorAction.setDefaultKeySequence(quickRefactorShortcut);
quickRefactorAction.setToolTip(Tr::tr("Refactor code using QodeAssist"));
quickRefactorAction.setText(Tr::tr("Quick Refactor with QodeAssist"));
quickRefactorAction.setIcon(QCODEASSIST_ICON.icon());
quickRefactorAction.addOnTriggered(this, [this] {
if (auto editor = TextEditor::TextEditorWidget::currentTextEditorWidget()) {
if (m_qodeAssistClient && m_qodeAssistClient->reachable()) {
QuickRefactorDialog
dialog(Core::ICore::dialogParent(), m_lastRefactorInstructions);
if (dialog.exec() == QDialog::Accepted) {
QString instructions = dialog.instructions();
if (!instructions.isEmpty()) {
m_lastRefactorInstructions = instructions;
m_qodeAssistClient->requestQuickRefactor(editor, instructions);
}
}
} else {
qWarning() << "The QodeAssist is not ready. Please check your connection and "
"settings.";
}
}
});
Core::ActionContainer *editorContextMenu = Core::ActionManager::actionContainer(
TextEditor::Constants::M_STANDARDCONTEXTMENU);
if (editorContextMenu) {
editorContextMenu->addSeparator(Core::Context(TextEditor::Constants::C_TEXTEDITOR));
editorContextMenu
->addAction(quickRefactorAction.command(), Core::Constants::G_DEFAULT_THREE);
editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE);
}
}
void extensionsInitialized() final {}
@ -195,6 +236,7 @@ private:
QPointer<Chat::NavigationPanel> m_navigationPanel;
QPointer<PluginUpdater> m_updater;
UpdateStatusWidget *m_statusWidget{nullptr};
QString m_lastRefactorInstructions;
};
} // namespace QodeAssist::Internal

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -47,8 +47,8 @@ ChatAssistantSettings::ChatAssistantSettings()
chatTokensThreshold.setLabelText(Tr::tr("Chat history token limit:"));
chatTokensThreshold.setToolTip(Tr::tr("Maximum number of tokens in chat history. When "
"exceeded, oldest messages will be removed."));
chatTokensThreshold.setRange(1, 900000);
chatTokensThreshold.setDefaultValue(8000);
chatTokensThreshold.setRange(1, 99999999);
chatTokensThreshold.setDefaultValue(20000);
linkOpenFiles.setSettingsKey(Constants::CA_LINK_OPEN_FILES);
linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default"));

View File

@ -218,6 +218,18 @@ CodeCompletionSettings::CodeCompletionSettings()
maxChangesCacheSize.setRange(2, 1000);
maxChangesCacheSize.setDefaultValue(10);
// Quick refactor command settings
useOpenFilesInQuickRefactor.setSettingsKey(Constants::CC_USE_OPEN_FILES_IN_QUICK_REFACTOR);
useOpenFilesInQuickRefactor.setLabelText(
Tr::tr("Include context from open files in quick refactor"));
useOpenFilesInQuickRefactor.setDefaultValue(false);
quickRefactorSystemPrompt.setSettingsKey(Constants::CC_QUICK_REFACTOR_SYSTEM_PROMPT);
quickRefactorSystemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
quickRefactorSystemPrompt.setDefaultValue(
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide"
"precise and contextually appropriate code completions to insert depending on user "
"instructions.\n\n");
// Ollama Settings
ollamaLivetime.setSettingsKey(Constants::CC_OLLAMA_LIVETIME);
ollamaLivetime.setToolTip(
@ -303,6 +315,10 @@ CodeCompletionSettings::CodeCompletionSettings()
Space{8},
Group{title(Tr::tr("Context Settings")), contextItem},
Space{8},
Group{
title(Tr::tr("Quick Refactor Settings")),
Column{useOpenFilesInQuickRefactor, quickRefactorSystemPrompt}},
Space{8},
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
Stretch{1}};
});
@ -371,6 +387,8 @@ void CodeCompletionSettings::resetSettingsToDefaults()
resetAspect(customLanguages);
resetAspect(showProgressWidget);
resetAspect(useOpenFilesContext);
resetAspect(useOpenFilesInQuickRefactor);
resetAspect(quickRefactorSystemPrompt);
}
}

View File

@ -77,6 +77,10 @@ public:
Utils::BoolAspect useProjectChangesCache{this};
Utils::IntegerAspect maxChangesCacheSize{this};
// Quick refactor command settings
Utils::BoolAspect useOpenFilesInQuickRefactor{this};
Utils::StringAspect quickRefactorSystemPrompt{this};
// Ollama Settings
Utils::StringAspect ollamaLivetime{this};
Utils::IntegerAspect contextWindow{this};

View File

@ -87,6 +87,14 @@ ProviderSettings::ProviderSettings()
mistralAiApiKey.setDefaultValue("");
mistralAiApiKey.setAutoApply(true);
codestralApiKey.setSettingsKey(Constants::CODESTRAL_API_KEY);
codestralApiKey.setLabelText(Tr::tr("Codestral API Key:"));
codestralApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
codestralApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
codestralApiKey.setHistoryCompleter(Constants::CODESTRAL_API_KEY_HISTORY);
codestralApiKey.setDefaultValue("");
codestralApiKey.setAutoApply(true);
// GoogleAI Settings
googleAiApiKey.setSettingsKey(Constants::GOOGLE_AI_API_KEY);
googleAiApiKey.setLabelText(Tr::tr("Google AI API Key:"));
@ -125,7 +133,7 @@ ProviderSettings::ProviderSettings()
Space{8},
Group{title(Tr::tr("Claude Settings")), Column{claudeApiKey}},
Space{8},
Group{title(Tr::tr("Mistral AI Settings")), Column{mistralAiApiKey}},
Group{title(Tr::tr("Mistral AI Settings")), Column{mistralAiApiKey, codestralApiKey}},
Space{8},
Group{title(Tr::tr("Google AI Settings")), Column{googleAiApiKey}},
Space{8},
@ -149,6 +157,9 @@ void ProviderSettings::setupConnections()
connect(&mistralAiApiKey, &ButtonAspect::changed, this, [this]() {
mistralAiApiKey.writeSettings();
});
connect(&codestralApiKey, &ButtonAspect::changed, this, [this]() {
codestralApiKey.writeSettings();
});
connect(&googleAiApiKey, &ButtonAspect::changed, this, [this]() {
googleAiApiKey.writeSettings();
});

View File

@ -38,6 +38,7 @@ public:
Utils::StringAspect claudeApiKey{this};
Utils::StringAspect openAiApiKey{this};
Utils::StringAspect mistralAiApiKey{this};
Utils::StringAspect codestralApiKey{this};
Utils::StringAspect googleAiApiKey{this};
Utils::StringAspect ollamaBasicAuthApiKey{this};

View File

@ -101,6 +101,8 @@ const char OPEN_AI_API_KEY[] = "QodeAssist.openAiApiKey";
const char OPEN_AI_API_KEY_HISTORY[] = "QodeAssist.openAiApiKeyHistory";
const char MISTRAL_AI_API_KEY[] = "QodeAssist.mistralAiApiKey";
const char MISTRAL_AI_API_KEY_HISTORY[] = "QodeAssist.mistralAiApiKeyHistory";
const char CODESTRAL_API_KEY[] = "QodeAssist.codestralApiKey";
const char CODESTRAL_API_KEY_HISTORY[] = "QodeAssist.codestralApiKeyHistory";
const char GOOGLE_AI_API_KEY[] = "QodeAssist.googleAiApiKey";
const char GOOGLE_AI_API_KEY_HISTORY[] = "QodeAssist.googleAiApiKeyHistory";
const char OLLAMA_BASIC_AUTH_API_KEY[] = "QodeAssist.ollamaBasicAuthApiKey";
@ -120,6 +122,10 @@ const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize";
const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt";
const char CA_SYSTEM_PROMPT[] = "QodeAssist.chatSystemPrompt";
// quick refactor command settings
const char CC_QUICK_REFACTOR_SYSTEM_PROMPT[] = "QodeAssist.ccQuickRefactorSystemPrompt";
const char CC_USE_OPEN_FILES_IN_QUICK_REFACTOR[] = "QodeAssist.ccUseOpenFilesInQuickRefactor";
// preset prompt settings
const char CC_TEMPERATURE[] = "QodeAssist.ccTemperature";
const char CC_MAX_TOKENS[] = "QodeAssist.ccMaxTokens";

View File

@ -30,7 +30,7 @@ class OllamaFim : public LLMCore::PromptTemplate
public:
LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIM; }
QString name() const override { return "Ollama FIM"; }
QStringList stopWords() const override { return QStringList(); }
QStringList stopWords() const override { return QStringList() << "<EOT>"; }
void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
{
request["prompt"] = context.prefix.value_or("");

View File

@ -0,0 +1,140 @@
/*
* 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 "EditorChatButton.hpp"
#include <utils/theme/theme.h>
#include <QMouseEvent>
#include <QPainter>
namespace QodeAssist {
EditorChatButton::EditorChatButton(QWidget *parent)
: QWidget(parent)
{
m_textColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
m_backgroundColor = Utils::creatorTheme()->color(Utils::Theme::BackgroundColorNormal);
m_logoPixmap = QPixmap(":/resources/images/qoderassist-icon.png");
if (!m_logoPixmap.isNull()) {
QImage image = m_logoPixmap.toImage();
image = image.convertToFormat(QImage::Format_ARGB32);
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
QColor pixelColor = QColor::fromRgba(image.pixel(x, y));
int brightness = (pixelColor.red() + pixelColor.green() + pixelColor.blue()) / 3;
if (brightness > 200) {
pixelColor.setAlpha(0);
image.setPixelColor(x, y, pixelColor);
} else if (pixelColor.alpha() > 0) {
int alpha = pixelColor.alpha();
pixelColor = m_textColor;
pixelColor.setAlpha(alpha);
image.setPixelColor(x, y, pixelColor);
}
}
}
m_logoPixmap = QPixmap::fromImage(image);
m_logoPixmap = m_logoPixmap.scaled(24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
setFixedSize(40, 40);
setCursor(Qt::PointingHandCursor);
setToolTip(tr("Open QodeAssist Chat"));
}
EditorChatButton::~EditorChatButton() = default;
void EditorChatButton::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QColor bgColor = m_backgroundColor;
if (m_isPressed) {
bgColor = bgColor.darker(120);
} else if (m_isHovered) {
bgColor = bgColor.lighter(110);
}
painter.fillRect(rect(), bgColor);
QRect buttonRect = rect().adjusted(4, 4, -4, -4);
painter.setPen(Qt::NoPen);
QColor buttonBgColor
= m_isPressed ? Utils::creatorTheme()->color(Utils::Theme::BackgroundColorHover).darker(110)
: Utils::creatorTheme()->color(Utils::Theme::BackgroundColorHover);
if (m_isHovered) {
buttonBgColor = buttonBgColor.lighter(110);
}
painter.setBrush(buttonBgColor);
painter.drawEllipse(buttonRect);
if (!m_logoPixmap.isNull()) {
QRect logoRect(
(width() - m_logoPixmap.width()) / 2,
(height() - m_logoPixmap.height()) / 2,
m_logoPixmap.width(),
m_logoPixmap.height());
painter.drawPixmap(logoRect, m_logoPixmap);
}
}
void EditorChatButton::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_isPressed = true;
update();
}
QWidget::mousePressEvent(event);
}
void EditorChatButton::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && m_isPressed) {
m_isPressed = false;
update();
if (rect().contains(event->pos())) {
emit clicked();
}
}
QWidget::mouseReleaseEvent(event);
}
void EditorChatButton::enterEvent(QEnterEvent *event)
{
m_isHovered = true;
update();
QWidget::enterEvent(event);
}
void EditorChatButton::leaveEvent(QEvent *event)
{
m_isHovered = false;
m_isPressed = false;
update();
QWidget::leaveEvent(event);
}
} // namespace QodeAssist

View File

@ -0,0 +1,53 @@
/*
* 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 <QPushButton>
#include <QTimer>
#include <QWidget>
namespace QodeAssist {
class EditorChatButton : public QWidget
{
Q_OBJECT
public:
explicit EditorChatButton(QWidget *parent = nullptr);
~EditorChatButton() override;
signals:
void clicked();
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
private:
QPixmap m_logoPixmap;
QColor m_textColor;
QColor m_backgroundColor;
bool m_isPressed = false;
bool m_isHovered = false;
};
} // namespace QodeAssist

View File

@ -0,0 +1,89 @@
/*
* 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 "EditorChatButtonHandler.hpp"
#include "EditorChatButton.hpp"
#include <texteditor/texteditor.h>
#include <utils/tooltip/tooltip.h>
#include <QPoint>
namespace QodeAssist {
EditorChatButtonHandler::~EditorChatButtonHandler()
{
delete m_chatButton;
}
void EditorChatButtonHandler::showButton(TextEditor::TextEditorWidget *widget)
{
if (!widget)
return;
m_widget = widget;
identifyMatch(widget, widget->textCursor().position(), [this](auto priority) {
if (priority != Priority_None && m_widget) {
const QTextCursor cursor = m_widget->textCursor();
const QRect selectionRect = m_widget->cursorRect(cursor);
m_cursorPosition = m_widget->viewport()->mapToGlobal(selectionRect.topLeft())
- Utils::ToolTip::offsetFromPosition();
operateTooltip(m_widget, m_cursorPosition);
}
});
}
void EditorChatButtonHandler::hideButton()
{
Utils::ToolTip::hide();
}
void EditorChatButtonHandler::identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
{
if (!editorWidget) {
report(Priority_None);
return;
}
report(Priority_Tooltip);
}
void EditorChatButtonHandler::operateTooltip(
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
{
if (!editorWidget)
return;
if (!Utils::ToolTip::isVisible()) {
m_chatButton = new EditorChatButton(editorWidget);
m_buttonHeight = m_chatButton->height();
QPoint showPoint = point;
showPoint.ry() -= m_buttonHeight;
Utils::ToolTip::show(showPoint, m_chatButton, editorWidget);
} else {
QPoint showPoint = point;
showPoint.ry() -= m_buttonHeight;
Utils::ToolTip::move(showPoint);
}
}
} // namespace QodeAssist

View File

@ -0,0 +1,52 @@
/*
* 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 "widgets/EditorChatButton.hpp"
#include <texteditor/basehoverhandler.h>
#include <QPointer>
namespace QodeAssist {
class EditorChatButtonHandler : public TextEditor::BaseHoverHandler
{
public:
explicit EditorChatButtonHandler() = default;
~EditorChatButtonHandler() override;
void showButton(TextEditor::TextEditorWidget *widget);
void hideButton();
signals:
void chatButtonClicked(TextEditor::TextEditorWidget *widget);
protected:
void identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override;
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
private:
QPointer<TextEditor::TextEditorWidget> m_widget;
QPoint m_cursorPosition;
EditorChatButton *m_chatButton = nullptr;
int m_buttonHeight = 0;
};
} // namespace QodeAssist

View File

@ -0,0 +1,217 @@
/*
* 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 "QuickRefactorDialog.hpp"
#include "QodeAssisttr.h"
#include <QApplication>
#include <QDialogButtonBox>
#include <QFontMetrics>
#include <QHBoxLayout>
#include <QLabel>
#include <QPlainTextEdit>
#include <QScreen>
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
#include <utils/theme/theme.h>
#include <utils/utilsicons.h>
namespace QodeAssist {
QuickRefactorDialog::QuickRefactorDialog(QWidget *parent, const QString &lastInstructions)
: QDialog(parent)
, m_lastInstructions(lastInstructions)
{
setWindowTitle(Tr::tr("Quick Refactor"));
setupUi();
QTimer::singleShot(0, this, &QuickRefactorDialog::updateDialogSize);
m_textEdit->installEventFilter(this);
updateDialogSize();
}
void QuickRefactorDialog::setupUi()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(10, 10, 10, 10);
mainLayout->setSpacing(8);
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->setSpacing(4);
createActionButtons();
actionsLayout->addWidget(m_repeatButton);
actionsLayout->addWidget(m_improveButton);
actionsLayout->addWidget(m_alternativeButton);
actionsLayout->addStretch();
mainLayout->addLayout(actionsLayout);
m_instructionsLabel = new QLabel(Tr::tr("Enter refactoring instructions:"), this);
mainLayout->addWidget(m_instructionsLabel);
m_textEdit = new QPlainTextEdit(this);
m_textEdit->setMinimumHeight(100);
m_textEdit->setPlaceholderText(Tr::tr("Type your refactoring instructions here..."));
connect(m_textEdit, &QPlainTextEdit::textChanged, this, &QuickRefactorDialog::updateDialogSize);
mainLayout->addWidget(m_textEdit);
QDialogButtonBox *buttonBox
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
}
void QuickRefactorDialog::createActionButtons()
{
Utils::Icon REPEAT_ICON(
{{":/resources/images/repeat-last-instruct-icon.png", Utils::Theme::IconsBaseColor}});
Utils::Icon IMPROVE_ICON(
{{":/resources/images/improve-current-code-icon.png", Utils::Theme::IconsBaseColor}});
Utils::Icon ALTER_ICON(
{{":/resources/images/suggest-new-icon.png", Utils::Theme::IconsBaseColor}});
m_repeatButton = new QToolButton(this);
m_repeatButton->setIcon(REPEAT_ICON.icon());
m_repeatButton->setToolTip(Tr::tr("Repeat Last Instructions"));
m_repeatButton->setEnabled(!m_lastInstructions.isEmpty());
connect(m_repeatButton, &QToolButton::clicked, this, &QuickRefactorDialog::useLastInstructions);
m_improveButton = new QToolButton(this);
m_improveButton->setIcon(IMPROVE_ICON.icon());
m_improveButton->setToolTip(Tr::tr("Improve Current Code"));
connect(
m_improveButton, &QToolButton::clicked, this, &QuickRefactorDialog::useImproveCodeTemplate);
m_alternativeButton = new QToolButton(this);
m_alternativeButton->setIcon(ALTER_ICON.icon());
m_alternativeButton->setToolTip(Tr::tr("Suggest Alternative Solution"));
connect(
m_alternativeButton,
&QToolButton::clicked,
this,
&QuickRefactorDialog::useAlternativeSolutionTemplate);
}
QString QuickRefactorDialog::instructions() const
{
return m_textEdit->toPlainText();
}
void QuickRefactorDialog::setInstructions(const QString &instructions)
{
m_textEdit->setPlainText(instructions);
}
QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
{
return m_selectedAction;
}
bool QuickRefactorDialog::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_textEdit && event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
if (keyEvent->modifiers() & Qt::ShiftModifier) {
return false;
}
accept();
return true;
}
}
return QDialog::eventFilter(watched, event);
}
void QuickRefactorDialog::useLastInstructions()
{
if (!m_lastInstructions.isEmpty()) {
m_textEdit->setPlainText(m_lastInstructions);
m_selectedAction = Action::RepeatLast;
}
accept();
}
void QuickRefactorDialog::useImproveCodeTemplate()
{
m_textEdit->setPlainText(Tr::tr(
"Improve the selected code by enhancing readability, efficiency, and maintainability. "
"Follow best practices for C++/Qt and fix any potential issues."));
m_selectedAction = Action::ImproveCode;
accept();
}
void QuickRefactorDialog::useAlternativeSolutionTemplate()
{
m_textEdit->setPlainText(
Tr::tr("Suggest an alternative implementation approach for the selected code. "
"Provide a different solution that might be cleaner, more efficient, "
"or uses different Qt/C++ patterns or idioms."));
m_selectedAction = Action::AlternativeSolution;
accept();
}
void QuickRefactorDialog::updateDialogSize()
{
QString text = m_textEdit->toPlainText();
QFontMetrics fm(m_textEdit->font());
QStringList lines = text.split('\n');
int lineCount = lines.size();
if (lineCount <= 1) {
int singleLineHeight = fm.height() + 10;
m_textEdit->setMinimumHeight(singleLineHeight);
m_textEdit->setMaximumHeight(singleLineHeight);
} else {
m_textEdit->setMaximumHeight(QWIDGETSIZE_MAX);
int lineHeight = fm.height() + 2;
int textEditHeight = qMin(qMax(lineCount, 2) * lineHeight, 20 * lineHeight);
m_textEdit->setMinimumHeight(textEditHeight);
}
int maxWidth = 500;
for (const QString &line : lines) {
int lineWidth = fm.horizontalAdvance(line) + 30;
maxWidth = qMax(maxWidth, qMin(lineWidth, 800));
}
QScreen *screen = QApplication::primaryScreen();
QRect screenGeometry = screen->availableGeometry();
int newWidth = qMin(maxWidth + 40, screenGeometry.width() * 3 / 4);
int newHeight;
if (lineCount <= 1) {
newHeight = 150;
} else {
newHeight = m_textEdit->minimumHeight() + 150;
}
newHeight = qMin(newHeight, screenGeometry.height() * 3 / 4);
resize(newWidth, newHeight);
}
} // namespace QodeAssist

View File

@ -0,0 +1,69 @@
/*
* 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 <QDialog>
#include <QString>
class QPlainTextEdit;
class QToolButton;
class QLabel;
namespace QodeAssist {
class QuickRefactorDialog : public QDialog
{
Q_OBJECT
public:
enum class Action { Custom, RepeatLast, ImproveCode, AlternativeSolution };
explicit QuickRefactorDialog(
QWidget *parent = nullptr, const QString &lastInstructions = QString());
~QuickRefactorDialog() override = default;
QString instructions() const;
void setInstructions(const QString &instructions);
Action selectedAction() const;
bool eventFilter(QObject *watched, QEvent *event) override;
private slots:
void useLastInstructions();
void useImproveCodeTemplate();
void useAlternativeSolutionTemplate();
void updateDialogSize();
private:
void setupUi();
void createActionButtons();
QPlainTextEdit *m_textEdit;
QToolButton *m_repeatButton;
QToolButton *m_improveButton;
QToolButton *m_alternativeButton;
QLabel *m_instructionsLabel;
Action m_selectedAction = Action::Custom;
QString m_lastInstructions;
};
} // namespace QodeAssist