mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-06-04 01:28:58 -04:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
af3fdb58ff | ||
|
637a4d9d4c | ||
|
7e2345773f | ||
|
14a5ddbdd8 | ||
|
e178b7daa7 | ||
|
4b353d5091 | ||
|
f7ba7b95be | ||
|
6ae95fec45 | ||
|
dad8ab2bf3 | ||
|
25a6983de0 | ||
|
4e05abc7d2 | ||
|
784529e344 | ||
|
155153a763 | ||
|
9225c0c1a9 | ||
|
43adc95857 | ||
|
ee672f2cda | ||
|
a3edb8a577 | ||
|
407d3b11c0 | ||
|
285e739074 | ||
|
f7e748ba7e | ||
|
acb1306321 | ||
|
8b38ecc29b | ||
|
cfb364f033 | ||
|
2fe6850a06 | ||
|
3e9506ca92 | ||
|
d24adff0f5 | ||
|
447324eb07 | ||
|
4ca494cc51 | ||
|
8a80dbe8f5 | ||
|
2b539bbdeb | ||
|
3f2c146df1 | ||
|
9a54f04a0d | ||
|
7a33425d1a | ||
|
711aa672f2 | ||
|
8cb6a2f6d2 | ||
|
2f9622e23e | ||
|
674b1fecde | ||
|
b36d01d2c7 | ||
|
615175bea8 | ||
|
7515599acb | ||
|
3652d4d5d9 | ||
|
75677770b2 | ||
|
329a1efd5d | ||
|
27760a3b99 |
29
.github/scripts/plugin.json
vendored
29
.github/scripts/plugin.json
vendored
@ -13,7 +13,7 @@
|
||||
"Linux"
|
||||
],
|
||||
"license": "GPLv3",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.11",
|
||||
"status": "draft",
|
||||
"is_pack": false,
|
||||
"released_at": null,
|
||||
@ -50,8 +50,33 @@
|
||||
},
|
||||
{
|
||||
"version": "0.5.7",
|
||||
"is_latest": true,
|
||||
"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",
|
||||
|
91
.github/workflows/build_cmake.yml
vendored
91
.github/workflows/build_cmake.yml
vendored
@ -12,16 +12,13 @@ on:
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: QodeAssist
|
||||
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"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.config.name }}
|
||||
name: ${{ matrix.config.name }} (Qt ${{ matrix.qt_config.qt_version }}, QtC ${{ matrix.qt_config.qt_creator_version }})
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
outputs:
|
||||
tag: ${{ steps.git.outputs.tag }}
|
||||
@ -47,12 +44,18 @@ jobs:
|
||||
platform: mac_x64,
|
||||
cc: "clang", cxx: "clang++"
|
||||
}
|
||||
qt_config:
|
||||
- {
|
||||
qt_version: "6.8.3",
|
||||
qt_creator_version: "16.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.8.3",
|
||||
qt_creator_version: "16.0.1"
|
||||
}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Checkout submodules
|
||||
id: git
|
||||
@ -61,7 +64,12 @@ jobs:
|
||||
if (${{github.ref}} MATCHES "tags/v(.*)")
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
|
||||
else()
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}")
|
||||
execute_process(
|
||||
COMMAND git rev-parse --short HEAD
|
||||
OUTPUT_VARIABLE short_sha
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${short_sha}")
|
||||
endif()
|
||||
|
||||
- name: Download Ninja and CMake
|
||||
@ -96,7 +104,7 @@ jobs:
|
||||
id: qt
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
set(qt_version "$ENV{QT_VERSION}")
|
||||
set(qt_version "${{ matrix.qt_config.qt_version }}")
|
||||
|
||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
@ -174,10 +182,11 @@ jobs:
|
||||
endif()
|
||||
|
||||
- name: Download Qt Creator
|
||||
uses: qt-creator/install-dev-package@v1.2
|
||||
uses: qt-creator/install-dev-package@v2.0
|
||||
with:
|
||||
version: ${{ env.QT_CREATOR_VERSION }}
|
||||
version: ${{ matrix.qt_config.qt_creator_version }}
|
||||
unzip-to: 'qtcreator'
|
||||
platform: ${{ matrix.config.platform }}
|
||||
|
||||
- name: Extract Qt Creator
|
||||
id: qt_creator
|
||||
@ -223,7 +232,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${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}"
|
||||
--src .
|
||||
--build build
|
||||
--qt-path "${{ steps.qt.outputs.qt_dir }}"
|
||||
@ -241,68 +250,18 @@ 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
|
||||
|
||||
# The json is the same for all platforms, but we need to save one
|
||||
- name: Upload plugin json
|
||||
if: startsWith(matrix.config.os, 'ubuntu')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PLUGIN_NAME }}-origin-json
|
||||
path: ./build/build/${{ env.PLUGIN_NAME }}.json
|
||||
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||
|
||||
- name: Run unit tests
|
||||
if: startsWith(matrix.config.os, 'ubuntu')
|
||||
run: |
|
||||
xvfb-run ./build/build/test/QodeAssistTest
|
||||
|
||||
update_json:
|
||||
if: contains(github.ref, 'tags/v')
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Download the JSON file
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PLUGIN_NAME }}-origin-json
|
||||
path: ./${{ env.PLUGIN_NAME }}-origin
|
||||
|
||||
- name: Store Release upload_url
|
||||
run: |
|
||||
RELEASE_HTML_URL=$(echo "${{github.event.repository.html_url}}/releases/download/v${{ needs.build.outputs.tag }}")
|
||||
echo "RELEASE_HTML_URL=${RELEASE_HTML_URL}" >> $GITHUB_ENV
|
||||
|
||||
- name: Run the Node.js script to update JSON
|
||||
env:
|
||||
QT_TOKEN: ${{ secrets.TOKEN }}
|
||||
API_URL: ${{ secrets.API_URL }}
|
||||
run: |
|
||||
node .github/scripts/registerPlugin.js ${{ env.RELEASE_HTML_URL }} ${{ env.PLUGIN_NAME }} ${{ env.QT_CREATOR_VERSION }} ${{ env.QT_CREATOR_VERSION_INTERNAL }} ${{ env.QT_TOKEN }} ${{ env.API_URL }}
|
||||
|
||||
- name: Delete previous json artifacts
|
||||
uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
name: ${{ env.PLUGIN_NAME }}*-json
|
||||
|
||||
- name: Upload the modified JSON file as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugin-json
|
||||
path: .github/scripts/${{ env.PLUGIN_NAME }}.json
|
||||
|
||||
release:
|
||||
if: contains(github.ref, 'tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, update_json]
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [build]
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
|
@ -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
|
||||
@ -105,6 +106,7 @@ add_qtc_plugin(QodeAssist
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -73,6 +73,8 @@ public:
|
||||
QString currentModel() const;
|
||||
QString lastMessageId() const;
|
||||
|
||||
Q_INVOKABLE void resetModelTo(int index);
|
||||
|
||||
signals:
|
||||
void tokensThresholdChanged();
|
||||
void modelReseted();
|
||||
|
@ -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();
|
||||
}
|
||||
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -25,7 +25,6 @@ TextEdit {
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.StyledText
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
}
|
||||
|
@ -57,6 +57,17 @@ LLMClientInterface::LLMClientInterface(
|
||||
&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
|
||||
@ -207,10 +218,8 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
: QString{"generateContent?"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
||||
} else {
|
||||
config.url = QUrl(QString("%1%2").arg(
|
||||
url,
|
||||
promptTemplate->type() == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
||||
: provider->chatEndpoint()));
|
||||
config.url = QUrl(
|
||||
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
|
||||
config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}};
|
||||
}
|
||||
config.apiKey = provider->apiKey();
|
||||
@ -289,6 +298,31 @@ LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
}
|
||||
|
||||
QString LLMClientInterface::endpoint(
|
||||
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
|
||||
{
|
||||
QString endpoint;
|
||||
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
|
||||
: m_generalSettings.ccEndpointMode.stringValue();
|
||||
if (endpointMode == "Auto") {
|
||||
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
||||
: provider->chatEndpoint();
|
||||
} else if (endpointMode == "Custom") {
|
||||
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||
: m_generalSettings.ccCustomEndpoint();
|
||||
} else if (endpointMode == "FIM") {
|
||||
endpoint = provider->completionEndpoint();
|
||||
} else if (endpointMode == "Chat") {
|
||||
endpoint = provider->chatEndpoint();
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
{
|
||||
return m_contextManager;
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendCompletionToClient(
|
||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
||||
{
|
||||
@ -322,7 +356,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);
|
||||
|
@ -62,6 +62,8 @@ public:
|
||||
// exposed for tests
|
||||
void sendData(const QByteArray &data) override;
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
protected:
|
||||
void startImpl() override;
|
||||
|
||||
@ -75,6 +77,7 @@ private:
|
||||
|
||||
LLMCore::ContextData prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
|
||||
|
||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||
const Settings::GeneralSettings &m_generalSettings;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.5.7",
|
||||
"Version" : "0.5.13",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||
|
@ -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>
|
||||
|
@ -49,6 +49,7 @@ namespace QodeAssist {
|
||||
|
||||
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||
: LanguageClient::Client(clientInterface)
|
||||
, m_llmClient(clientInterface)
|
||||
, m_recentCharCount(0)
|
||||
{
|
||||
setName("QodeAssist");
|
||||
@ -152,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;
|
||||
@ -181,6 +190,14 @@ void QodeAssistClient::requestQuickRefactor(
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
|
||||
if (m_llmClient->contextManager()
|
||||
->ignoreManager()
|
||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_refactorHandler) {
|
||||
m_refactorHandler = new QuickRefactorHandler(this);
|
||||
connect(
|
||||
@ -327,7 +344,6 @@ void QodeAssistClient::cleanupConnections()
|
||||
|
||||
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
||||
{
|
||||
m_progressHandler.hideProgress();
|
||||
if (!result.success) {
|
||||
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
|
||||
return;
|
||||
@ -352,5 +368,6 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
||||
|
||||
cursor.insertText(result.newText);
|
||||
cursor.endEditBlock();
|
||||
m_progressHandler.hideProgress();
|
||||
}
|
||||
} // namespace QodeAssist
|
||||
|
@ -69,6 +69,7 @@ private:
|
||||
CompletionProgressHandler m_progressHandler;
|
||||
EditorChatButtonHandler m_chatButtonHandler;
|
||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||
LLMClientInterface *m_llmClient;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
66
README.md
66
README.md
@ -2,7 +2,8 @@
|
||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/BGMkUsXUgf)
|
||||
|
||||
 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.
|
||||
@ -23,10 +24,12 @@
|
||||
7. [Configure for Ollama](#configure-for-ollama)
|
||||
8. [Configure for llama.cpp](#configure-for-llamacpp)
|
||||
9. [System Prompt Configuration](#system-prompt-configuration)
|
||||
10. [File Context Features](#file-context-features)
|
||||
11. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
12. [Development Progress](#development-progress)
|
||||
13. [Hotkeys](#hotkeys)
|
||||
10. [File Context Feature](#file-context-feature)
|
||||
11. [Quick Refactoring Feature](#quick-refactoring-feature)
|
||||
12. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
13. [Development Progress](#development-progress)
|
||||
14. [Hotkeys](#hotkeys)
|
||||
15. [Ignoring Files](#ignoring-files)
|
||||
14. [Troubleshooting](#troubleshooting)
|
||||
15. [Support the Development](#support-the-development-of-qodeassist)
|
||||
16. [How to Build](#how-to-build)
|
||||
@ -35,6 +38,7 @@
|
||||
|
||||
- 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
|
||||
@ -61,6 +65,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">
|
||||
@ -195,7 +204,7 @@ You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
|
||||
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings.
|
||||
|
||||
## File Context Features
|
||||
## File Context Feature
|
||||
|
||||
QodeAssist provides two powerful ways to include source code files in your chat conversations: Attachments and Linked Files. Each serves a distinct purpose and helps provide better context for the AI assistant.
|
||||
|
||||
@ -228,9 +237,23 @@ Linked files provide persistent context throughout the conversation:
|
||||
- Supports automatic syncing with open editor files (can be enabled in settings)
|
||||
- Files can be added/removed at any time during the conversation
|
||||
|
||||
## Quick Refactoring Feature
|
||||
### Setup
|
||||
Since this is actually a small chat with redirected output, the main settings of the provider, model and template are taken from the chat settings
|
||||
### Using
|
||||
The request to model consist of instructions to model, selection code and cursor position
|
||||
The default instruction is: "Refactor the code to improve its quality and maintainability." and sending if text field is empty
|
||||
Also there buttons to quick call instractions:
|
||||
* Repeat latest instruction, will activate after sending first request in QtCreator session
|
||||
* Improve current selection code
|
||||
* Suggestion alternative variant of selection code
|
||||
* Other instructions[TBD]
|
||||
|
||||
## QtCreator Version Compatibility
|
||||
|
||||
- QtCreator 16.0.0 - 0.5.2 - 0.5.x
|
||||
- QtCreator 16.0.2 - 0.5.13 - 0.x.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
|
||||
@ -244,6 +267,7 @@ Linked files provide persistent context throughout the conversation:
|
||||
- [x] Sharing diff with model
|
||||
- [ ] Sharing project source with model
|
||||
- [ ] Support for more providers and models
|
||||
- [ ] Support MCP
|
||||
|
||||
## Hotkeys
|
||||
|
||||
@ -253,6 +277,34 @@ 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
|
||||
*.tmp
|
||||
# Ignore a specific file
|
||||
src/generated/autogen.cpp
|
||||
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
274
context/IgnoreManager.cpp
Normal 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
62
context/IgnoreManager.hpp
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <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
|
@ -22,14 +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(
|
||||
@ -37,12 +74,13 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
|
||||
QString::fromUtf8(
|
||||
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QNetworkAccessManager *manager = new QNetworkAccessManager();
|
||||
QNetworkRequest networkRequest(config.url);
|
||||
networkRequest.setTransferTimeout(300000);
|
||||
|
||||
config.provider->prepareNetworkRequest(networkRequest);
|
||||
|
||||
QNetworkReply *reply
|
||||
= manager->post(networkRequest, QJsonDocument(config.providerRequest).toJson());
|
||||
= m_manager->post(networkRequest, QJsonDocument(config.providerRequest).toJson());
|
||||
if (!reply) {
|
||||
LOG_MESSAGE("Error: Failed to create network reply");
|
||||
return;
|
||||
@ -55,24 +93,28 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
|
||||
handleLLMResponse(reply, request, config);
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, requestId, manager]() {
|
||||
m_activeRequests.remove(requestId);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QString errorMessage = reply->errorString();
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
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));
|
||||
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());
|
||||
}
|
||||
emit requestFinished(requestId, false, errorMessage);
|
||||
} else {
|
||||
LOG_MESSAGE("Request finished successfully");
|
||||
emit requestFinished(requestId, true, QString());
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
});
|
||||
reply->deleteLater();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void RequestHandler::handleLLMResponse(
|
||||
@ -102,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(
|
||||
|
@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
|
||||
@ -32,16 +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:
|
||||
QMap<QString, QNetworkReply *> m_activeRequests;
|
||||
QMap<QNetworkReply *, QString> m_accumulatedResponses;
|
||||
QNetworkAccessManager *m_manager;
|
||||
QMutex m_mutex;
|
||||
|
||||
bool processSingleLineCompletion(
|
||||
QNetworkReply *reply,
|
||||
|
46
providers/CodestralProvider.cpp
Normal file
46
providers/CodestralProvider.cpp
Normal 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
|
35
providers/CodestralProvider.hpp
Normal file
35
providers/CodestralProvider.hpp
Normal 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
|
@ -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
|
||||
|
@ -56,6 +56,7 @@
|
||||
#include "settings/ProjectSettingsPanel.hpp"
|
||||
#include "settings/SettingsConstants.hpp"
|
||||
#include "templates/Templates.hpp"
|
||||
#include "widgets/QuickRefactorDialog.hpp"
|
||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <texteditor/textdocument.h>
|
||||
@ -148,17 +149,17 @@ public:
|
||||
quickRefactorAction.setIcon(QCODEASSIST_ICON.icon());
|
||||
quickRefactorAction.addOnTriggered(this, [this] {
|
||||
if (auto editor = TextEditor::TextEditorWidget::currentTextEditorWidget()) {
|
||||
bool ok;
|
||||
if (m_qodeAssistClient && m_qodeAssistClient->reachable()) {
|
||||
QString instructions = QInputDialog::getText(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Quick Refactor"),
|
||||
Tr::tr("Enter refactoring instructions:"),
|
||||
QLineEdit::Normal,
|
||||
QString(),
|
||||
&ok);
|
||||
if (ok)
|
||||
m_qodeAssistClient->requestQuickRefactor(editor, instructions);
|
||||
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.";
|
||||
@ -235,6 +236,7 @@ private:
|
||||
QPointer<Chat::NavigationPanel> m_navigationPanel;
|
||||
QPointer<PluginUpdater> m_updater;
|
||||
UpdateStatusWidget *m_statusWidget{nullptr};
|
||||
QString m_lastRefactorInstructions;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Internal
|
||||
|
BIN
resources/images/improve-current-code-icon.png
Normal file
BIN
resources/images/improve-current-code-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
resources/images/improve-current-code-icon@2x.png
Normal file
BIN
resources/images/improve-current-code-icon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/images/repeat-last-instruct-icon.png
Normal file
BIN
resources/images/repeat-last-instruct-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
resources/images/repeat-last-instruct-icon@2x.png
Normal file
BIN
resources/images/repeat-last-instruct-icon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
resources/images/suggest-new-icon.png
Normal file
BIN
resources/images/suggest-new-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 963 B |
BIN
resources/images/suggest-new-icon@2x.png
Normal file
BIN
resources/images/suggest-new-icon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
@ -22,6 +22,8 @@
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
@ -47,8 +49,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"));
|
||||
@ -137,6 +139,65 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
contextWindow.setRange(-1, 10000);
|
||||
contextWindow.setDefaultValue(2048);
|
||||
|
||||
autosave.setDefaultValue(true);
|
||||
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
|
||||
|
||||
textFontFamily.setSettingsKey(Constants::CA_TEXT_FONT_FAMILY);
|
||||
textFontFamily.setLabelText(Tr::tr("Text Font:"));
|
||||
textFontFamily.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
const QStringList families = QFontDatabase::families();
|
||||
for (const QString &family : families) {
|
||||
textFontFamily.addOption(family);
|
||||
}
|
||||
textFontFamily.setDefaultValue(QApplication::font().family());
|
||||
|
||||
textFontSize.setSettingsKey(Constants::CA_TEXT_FONT_SIZE);
|
||||
textFontSize.setLabelText(Tr::tr("Text Font Size:"));
|
||||
textFontSize.setDefaultValue(QApplication::font().pointSize());
|
||||
|
||||
codeFontFamily.setSettingsKey(Constants::CA_CODE_FONT_FAMILY);
|
||||
codeFontFamily.setLabelText(Tr::tr("Code Font:"));
|
||||
codeFontFamily.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
const QStringList monospaceFamilies = QFontDatabase::families(QFontDatabase::Latin);
|
||||
for (const QString &family : monospaceFamilies) {
|
||||
if (QFontDatabase::isFixedPitch(family)) {
|
||||
codeFontFamily.addOption(family);
|
||||
}
|
||||
}
|
||||
|
||||
QString defaultMonoFont;
|
||||
QStringList fixedPitchFamilies;
|
||||
|
||||
for (const QString &family : monospaceFamilies) {
|
||||
if (QFontDatabase::isFixedPitch(family)) {
|
||||
fixedPitchFamilies.append(family);
|
||||
}
|
||||
}
|
||||
|
||||
if (fixedPitchFamilies.contains("Consolas")) {
|
||||
defaultMonoFont = "Consolas";
|
||||
} else if (fixedPitchFamilies.contains("Courier New")) {
|
||||
defaultMonoFont = "Courier New";
|
||||
} else if (fixedPitchFamilies.contains("Monospace")) {
|
||||
defaultMonoFont = "Monospace";
|
||||
} else if (!fixedPitchFamilies.isEmpty()) {
|
||||
defaultMonoFont = fixedPitchFamilies.first();
|
||||
} else {
|
||||
defaultMonoFont = QApplication::font().family();
|
||||
}
|
||||
codeFontFamily.setDefaultValue(defaultMonoFont);
|
||||
codeFontSize.setSettingsKey(Constants::CA_CODE_FONT_SIZE);
|
||||
codeFontSize.setLabelText(Tr::tr("Code Font Size:"));
|
||||
codeFontSize.setDefaultValue(QApplication::font().pointSize());
|
||||
|
||||
textFormat.setSettingsKey(Constants::CA_TEXT_FORMAT);
|
||||
textFormat.setLabelText(Tr::tr("Text Format:"));
|
||||
textFormat.setDefaultValue(0);
|
||||
textFormat.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
textFormat.addOption("Markdown");
|
||||
textFormat.addOption("HTML");
|
||||
textFormat.addOption("Plain Text");
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
|
||||
readSettings();
|
||||
@ -160,6 +221,11 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
ollamaGrid.addRow({ollamaLivetime});
|
||||
ollamaGrid.addRow({contextWindow});
|
||||
|
||||
auto chatViewSettingsGrid = Grid{};
|
||||
chatViewSettingsGrid.addRow({textFontFamily, textFontSize});
|
||||
chatViewSettingsGrid.addRow({codeFontFamily, codeFontSize});
|
||||
chatViewSettingsGrid.addRow({textFormat});
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
@ -181,6 +247,7 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
systemPrompt,
|
||||
}},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
@ -221,6 +288,11 @@ void ChatAssistantSettings::resetSettingsToDefaults()
|
||||
resetAspect(ollamaLivetime);
|
||||
resetAspect(contextWindow);
|
||||
resetAspect(linkOpenFiles);
|
||||
resetAspect(textFontFamily);
|
||||
resetAspect(codeFontFamily);
|
||||
resetAspect(textFontSize);
|
||||
resetAspect(codeFontSize);
|
||||
resetAspect(textFormat);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,13 @@ public:
|
||||
Utils::StringAspect ollamaLivetime{this};
|
||||
Utils::IntegerAspect contextWindow{this};
|
||||
|
||||
// Visuals settings
|
||||
Utils::SelectionAspect textFontFamily{this};
|
||||
Utils::IntegerAspect textFontSize{this};
|
||||
Utils::SelectionAspect codeFontFamily{this};
|
||||
Utils::IntegerAspect codeFontSize{this};
|
||||
Utils::SelectionAspect textFormat{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
|
@ -99,9 +99,20 @@ GeneralSettings::GeneralSettings()
|
||||
ccSelectTemplate.m_buttonText = TrConstants::SELECT;
|
||||
|
||||
initStringAspect(ccUrl, Constants::CC_URL, TrConstants::URL, "http://localhost:11434");
|
||||
ccUrl.setHistoryCompleter(Constants::CC_URL_HISTORY);
|
||||
ccUrl.setHistoryCompleter(Constants::CC_CUSTOM_ENDPOINT_HISTORY);
|
||||
ccSetUrl.m_buttonText = TrConstants::SELECT;
|
||||
|
||||
ccEndpointMode.setSettingsKey(Constants::CC_ENDPOINT_MODE);
|
||||
ccEndpointMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
ccEndpointMode.addOption("Auto");
|
||||
ccEndpointMode.addOption("Custom");
|
||||
ccEndpointMode.addOption("FIM");
|
||||
ccEndpointMode.addOption("Chat");
|
||||
ccEndpointMode.setDefaultValue("Auto");
|
||||
|
||||
initStringAspect(ccCustomEndpoint, Constants::CC_CUSTOM_ENDPOINT, TrConstants::ENDPOINT_MODE, "");
|
||||
ccCustomEndpoint.setHistoryCompleter(Constants::CC_CUSTOM_ENDPOINT_HISTORY);
|
||||
|
||||
ccStatus.setDisplayStyle(Utils::StringAspect::LabelDisplay);
|
||||
ccStatus.setLabelText(TrConstants::STATUS);
|
||||
ccStatus.setDefaultValue("");
|
||||
@ -134,6 +145,21 @@ GeneralSettings::GeneralSettings()
|
||||
ccPreset1Url.setHistoryCompleter(Constants::CC_PRESET1_URL_HISTORY);
|
||||
ccPreset1SetUrl.m_buttonText = TrConstants::SELECT;
|
||||
|
||||
ccPreset1EndpointMode.setSettingsKey(Constants::CC_PRESET1_ENDPOINT_MODE);
|
||||
ccPreset1EndpointMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
ccPreset1EndpointMode.addOption("Auto");
|
||||
ccPreset1EndpointMode.addOption("Custom");
|
||||
ccPreset1EndpointMode.addOption("FIM");
|
||||
ccPreset1EndpointMode.addOption("Chat");
|
||||
ccPreset1EndpointMode.setDefaultValue("Auto");
|
||||
|
||||
initStringAspect(
|
||||
ccPreset1CustomEndpoint,
|
||||
Constants::CC_PRESET1_CUSTOM_ENDPOINT,
|
||||
TrConstants::ENDPOINT_MODE,
|
||||
"");
|
||||
ccPreset1CustomEndpoint.setHistoryCompleter(Constants::CC_PRESET1_CUSTOM_ENDPOINT_HISTORY);
|
||||
|
||||
initStringAspect(
|
||||
ccPreset1Model, Constants::CC_PRESET1_MODEL, TrConstants::MODEL, "qwen2.5-coder:7b");
|
||||
ccPreset1Model.setHistoryCompleter(Constants::CC_PRESET1_MODEL_HISTORY);
|
||||
@ -162,6 +188,17 @@ GeneralSettings::GeneralSettings()
|
||||
caUrl.setHistoryCompleter(Constants::CA_URL_HISTORY);
|
||||
caSetUrl.m_buttonText = TrConstants::SELECT;
|
||||
|
||||
caEndpointMode.setSettingsKey(Constants::CA_ENDPOINT_MODE);
|
||||
caEndpointMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
caEndpointMode.addOption("Auto");
|
||||
caEndpointMode.addOption("Custom");
|
||||
caEndpointMode.addOption("FIM");
|
||||
caEndpointMode.addOption("Chat");
|
||||
caEndpointMode.setDefaultValue("Auto");
|
||||
|
||||
initStringAspect(caCustomEndpoint, Constants::CA_CUSTOM_ENDPOINT, TrConstants::ENDPOINT_MODE, "");
|
||||
caCustomEndpoint.setHistoryCompleter(Constants::CA_CUSTOM_ENDPOINT_HISTORY);
|
||||
|
||||
caStatus.setDisplayStyle(Utils::StringAspect::LabelDisplay);
|
||||
caStatus.setLabelText(TrConstants::STATUS);
|
||||
caStatus.setDefaultValue("");
|
||||
@ -179,6 +216,9 @@ GeneralSettings::GeneralSettings()
|
||||
setupConnections();
|
||||
|
||||
updatePreset1Visiblity(specifyPreset1.value());
|
||||
ccCustomEndpoint.setEnabled(ccEndpointMode.stringValue() == "Custom");
|
||||
ccPreset1CustomEndpoint.setEnabled(ccPreset1EndpointMode.stringValue() == "Custom");
|
||||
caCustomEndpoint.setEnabled(caEndpointMode.stringValue() == "Custom");
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
@ -186,18 +226,21 @@ GeneralSettings::GeneralSettings()
|
||||
auto ccGrid = Grid{};
|
||||
ccGrid.addRow({ccProvider, ccSelectProvider});
|
||||
ccGrid.addRow({ccUrl, ccSetUrl});
|
||||
ccGrid.addRow({ccCustomEndpoint, ccEndpointMode});
|
||||
ccGrid.addRow({ccModel, ccSelectModel});
|
||||
ccGrid.addRow({ccTemplate, ccSelectTemplate});
|
||||
|
||||
auto ccPreset1Grid = Grid{};
|
||||
ccPreset1Grid.addRow({ccPreset1Provider, ccPreset1SelectProvider});
|
||||
ccPreset1Grid.addRow({ccPreset1Url, ccPreset1SetUrl});
|
||||
ccPreset1Grid.addRow({ccPreset1CustomEndpoint, ccPreset1EndpointMode});
|
||||
ccPreset1Grid.addRow({ccPreset1Model, ccPreset1SelectModel});
|
||||
ccPreset1Grid.addRow({ccPreset1Template, ccPreset1SelectTemplate});
|
||||
|
||||
auto caGrid = Grid{};
|
||||
caGrid.addRow({caProvider, caSelectProvider});
|
||||
caGrid.addRow({caUrl, caSetUrl});
|
||||
caGrid.addRow({caCustomEndpoint, caEndpointMode});
|
||||
caGrid.addRow({caModel, caSelectModel});
|
||||
caGrid.addRow({caTemplate, caSelectTemplate});
|
||||
|
||||
@ -389,6 +432,8 @@ void GeneralSettings::updatePreset1Visiblity(bool state)
|
||||
ccPreset1SelectModel.updateVisibility(specifyPreset1.volatileValue());
|
||||
ccPreset1Template.setVisible(specifyPreset1.volatileValue());
|
||||
ccPreset1SelectTemplate.updateVisibility(specifyPreset1.volatileValue());
|
||||
ccPreset1EndpointMode.setVisible(specifyPreset1.volatileValue());
|
||||
ccPreset1CustomEndpoint.setVisible(specifyPreset1.volatileValue());
|
||||
}
|
||||
|
||||
void GeneralSettings::setupConnections()
|
||||
@ -404,6 +449,19 @@ void GeneralSettings::setupConnections()
|
||||
connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
updatePreset1Visiblity(specifyPreset1.volatileValue());
|
||||
});
|
||||
connect(&ccEndpointMode, &Utils::BaseAspect::volatileValueChanged, this, [this]() {
|
||||
ccCustomEndpoint.setEnabled(
|
||||
ccEndpointMode.volatileValue() == ccEndpointMode.indexForDisplay("Custom"));
|
||||
});
|
||||
connect(&ccPreset1EndpointMode, &Utils::BaseAspect::volatileValueChanged, this, [this]() {
|
||||
ccPreset1CustomEndpoint.setEnabled(
|
||||
ccPreset1EndpointMode.volatileValue()
|
||||
== ccPreset1EndpointMode.indexForDisplay("Custom"));
|
||||
});
|
||||
connect(&caEndpointMode, &Utils::BaseAspect::volatileValueChanged, this, [this]() {
|
||||
caCustomEndpoint.setEnabled(
|
||||
caEndpointMode.volatileValue() == caEndpointMode.indexForDisplay("Custom"));
|
||||
});
|
||||
}
|
||||
|
||||
void GeneralSettings::resetPageToDefaults()
|
||||
@ -433,6 +491,12 @@ void GeneralSettings::resetPageToDefaults()
|
||||
resetAspect(ccPreset1Model);
|
||||
resetAspect(ccPreset1Template);
|
||||
resetAspect(ccPreset1Url);
|
||||
resetAspect(ccEndpointMode);
|
||||
resetAspect(ccCustomEndpoint);
|
||||
resetAspect(ccPreset1EndpointMode);
|
||||
resetAspect(ccPreset1CustomEndpoint);
|
||||
resetAspect(caEndpointMode);
|
||||
resetAspect(caCustomEndpoint);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,9 @@ public:
|
||||
Utils::StringAspect ccUrl{this};
|
||||
ButtonAspect ccSetUrl{this};
|
||||
|
||||
Utils::SelectionAspect ccEndpointMode{this};
|
||||
Utils::StringAspect ccCustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect ccStatus{this};
|
||||
ButtonAspect ccTest{this};
|
||||
|
||||
@ -70,6 +73,9 @@ public:
|
||||
Utils::StringAspect ccPreset1Url{this};
|
||||
ButtonAspect ccPreset1SetUrl{this};
|
||||
|
||||
Utils::SelectionAspect ccPreset1EndpointMode{this};
|
||||
Utils::StringAspect ccPreset1CustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect ccPreset1Model{this};
|
||||
ButtonAspect ccPreset1SelectModel{this};
|
||||
|
||||
@ -89,6 +95,9 @@ public:
|
||||
Utils::StringAspect caUrl{this};
|
||||
ButtonAspect caSetUrl{this};
|
||||
|
||||
Utils::SelectionAspect caEndpointMode{this};
|
||||
Utils::StringAspect caCustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect caStatus{this};
|
||||
ButtonAspect caTest{this};
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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};
|
||||
|
||||
|
@ -35,6 +35,9 @@ const char CC_MODEL_HISTORY[] = "QodeAssist.ccModelHistory";
|
||||
const char CC_TEMPLATE[] = "QodeAssist.ccTemplate";
|
||||
const char CC_URL[] = "QodeAssist.ccUrl";
|
||||
const char CC_URL_HISTORY[] = "QodeAssist.ccUrlHistory";
|
||||
const char CC_ENDPOINT_MODE[] = "QodeAssist.ccEndpointMode";
|
||||
const char CC_CUSTOM_ENDPOINT[] = "QodeAssist.ccCustomEndpoint";
|
||||
const char CC_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.ccCustomEndpointHistory";
|
||||
|
||||
const char CA_PROVIDER[] = "QodeAssist.caProvider";
|
||||
const char CA_MODEL[] = "QodeAssist.caModel";
|
||||
@ -42,6 +45,9 @@ const char CA_MODEL_HISTORY[] = "QodeAssist.caModelHistory";
|
||||
const char CA_TEMPLATE[] = "QodeAssist.caTemplate";
|
||||
const char CA_URL[] = "QodeAssist.caUrl";
|
||||
const char CA_URL_HISTORY[] = "QodeAssist.caUrlHistory";
|
||||
const char CA_ENDPOINT_MODE[] = "QodeAssist.caEndpointMode";
|
||||
const char CA_CUSTOM_ENDPOINT[] = "QodeAssist.caCustomEndpoint";
|
||||
const char CA_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.caCustomEndpointHistory";
|
||||
|
||||
const char CC_SPECIFY_PRESET1[] = "QodeAssist.ccSpecifyPreset1";
|
||||
const char CC_PRESET1_LANGUAGE[] = "QodeAssist.ccPreset1Language";
|
||||
@ -51,6 +57,9 @@ const char CC_PRESET1_MODEL_HISTORY[] = "QodeAssist.ccPreset1ModelHistory";
|
||||
const char CC_PRESET1_TEMPLATE[] = "QodeAssist.ccPreset1Template";
|
||||
const char CC_PRESET1_URL[] = "QodeAssist.ccPreset1Url";
|
||||
const char CC_PRESET1_URL_HISTORY[] = "QodeAssist.ccPreset1UrlHistory";
|
||||
const char CC_PRESET1_ENDPOINT_MODE[] = "QodeAssist.caPreset1EndpointMode";
|
||||
const char CC_PRESET1_CUSTOM_ENDPOINT[] = "QodeAssist.caPreset1CustomEndpointHistory";
|
||||
const char CC_PRESET1_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.caPreset1CustomEndpointHistory";
|
||||
|
||||
// settings
|
||||
const char ENABLE_QODE_ASSIST[] = "QodeAssist.enableQodeAssist";
|
||||
@ -101,6 +110,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";
|
||||
@ -149,5 +160,10 @@ const char CA_USE_FREQUENCY_PENALTY[] = "QodeAssist.chatUseFrequencyPenalty";
|
||||
const char CA_FREQUENCY_PENALTY[] = "QodeAssist.chatFrequencyPenalty";
|
||||
const char CA_OLLAMA_LIVETIME[] = "QodeAssist.chatOllamaLivetime";
|
||||
const char CA_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.caOllamaContextWindow";
|
||||
const char CA_TEXT_FONT_FAMILY[] = "QodeAssist.caTextFontFamily";
|
||||
const char CA_TEXT_FONT_SIZE[] = "QodeAssist.caTextFontSize";
|
||||
const char CA_CODE_FONT_FAMILY[] = "QodeAssist.caCodeFontFamily";
|
||||
const char CA_CODE_FONT_SIZE[] = "QodeAssist.caCodeFontSize";
|
||||
const char CA_TEXT_FORMAT[] = "QodeAssist.caTextFormat";
|
||||
|
||||
} // namespace QodeAssist::Constants
|
||||
|
@ -44,6 +44,7 @@ inline const char *ENABLE_CHECK_UPDATE_ON_START
|
||||
inline const char *ENABLE_CHAT = QT_TRANSLATE_NOOP(
|
||||
"QtC::QodeAssist",
|
||||
"Enable Chat(If you have performance issues try disabling this, need restart QtC)");
|
||||
inline const char *ENDPOINT_MODE = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Endpoint Mode:");
|
||||
|
||||
inline const char *CODE_COMPLETION = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Code Completion");
|
||||
inline const char *CHAT_ASSISTANT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Chat Assistant");
|
||||
|
@ -38,7 +38,6 @@ namespace QodeAssist {
|
||||
void CompletionProgressHandler::showProgress(TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
m_widget = widget;
|
||||
m_isActive = true;
|
||||
|
||||
if (m_widget) {
|
||||
const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor());
|
||||
@ -54,14 +53,13 @@ void CompletionProgressHandler::showProgress(TextEditor::TextEditorWidget *widge
|
||||
|
||||
void CompletionProgressHandler::hideProgress()
|
||||
{
|
||||
m_isActive = false;
|
||||
Utils::ToolTip::hide();
|
||||
Utils::ToolTip::hideImmediately();
|
||||
}
|
||||
|
||||
void CompletionProgressHandler::identifyMatch(
|
||||
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
|
||||
{
|
||||
if (!m_isActive || !editorWidget) {
|
||||
if (!editorWidget) {
|
||||
report(Priority_None);
|
||||
return;
|
||||
}
|
||||
@ -72,7 +70,7 @@ void CompletionProgressHandler::identifyMatch(
|
||||
void CompletionProgressHandler::operateTooltip(
|
||||
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
|
||||
{
|
||||
if (!m_isActive || !editorWidget)
|
||||
if (!editorWidget)
|
||||
return;
|
||||
|
||||
auto progressWidget = new ProgressWidget(editorWidget);
|
||||
|
@ -38,7 +38,6 @@ protected:
|
||||
private:
|
||||
QPointer<TextEditor::TextEditorWidget> m_widget;
|
||||
QPoint m_iconPosition;
|
||||
bool m_isActive = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
217
widgets/QuickRefactorDialog.cpp
Normal file
217
widgets/QuickRefactorDialog.cpp
Normal 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
|
69
widgets/QuickRefactorDialog.hpp
Normal file
69
widgets/QuickRefactorDialog.hpp
Normal 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
|
Loading…
x
Reference in New Issue
Block a user