mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-12 10:10:44 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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",
|
||||
|
||||
6
.github/workflows/build_cmake.yml
vendored
6
.github/workflows/build_cmake.yml
vendored
@ -223,7 +223,7 @@ jobs:
|
||||
COMMAND python
|
||||
-u
|
||||
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
|
||||
--name "$ENV{PLUGIN_NAME}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
|
||||
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
|
||||
--src .
|
||||
--build build
|
||||
--qt-path "${{ steps.qt.outputs.qt_dir }}"
|
||||
@ -241,8 +241,8 @@ jobs:
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
|
||||
# The json is the same for all platforms, but we need to save one
|
||||
- name: Upload plugin json
|
||||
|
||||
@ -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
|
||||
@ -289,6 +300,11 @@ LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
}
|
||||
|
||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
{
|
||||
return m_contextManager;
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendCompletionToClient(
|
||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
||||
{
|
||||
@ -322,7 +338,9 @@ void LLMClientInterface::sendCompletionToClient(
|
||||
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
|
||||
QJsonObject range;
|
||||
range["start"] = position;
|
||||
range["end"] = position;
|
||||
QJsonObject end = position;
|
||||
end["character"] = position["character"].toInt() + processedCompletion.length();
|
||||
range["end"] = end;
|
||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||
completions.append(completionItem);
|
||||
|
||||
@ -62,6 +62,8 @@ public:
|
||||
// exposed for tests
|
||||
void sendData(const QByteArray &data) override;
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
protected:
|
||||
void startImpl() override;
|
||||
|
||||
|
||||
@ -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.12",
|
||||
"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
|
||||
|
||||
52
README.md
52
README.md
@ -2,7 +2,7 @@
|
||||
[](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.
|
||||
@ -27,6 +27,7 @@
|
||||
11. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
12. [Development Progress](#development-progress)
|
||||
13. [Hotkeys](#hotkeys)
|
||||
14. [Ignoring Files](#ignoring-files)
|
||||
14. [Troubleshooting](#troubleshooting)
|
||||
15. [Support the Development](#support-the-development-of-qodeassist)
|
||||
16. [How to Build](#how-to-build)
|
||||
@ -35,6 +36,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 +63,11 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
|
||||
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Quick refactor in code: (click to expand)</summary>
|
||||
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Multiline Code completion: (click to expand)</summary>
|
||||
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
|
||||
@ -230,7 +237,8 @@ Linked files provide persistent context throughout the conversation:
|
||||
|
||||
## QtCreator Version Compatibility
|
||||
|
||||
- QtCreator 16.0.0 - 0.5.2 - 0.5.x
|
||||
- QtCreator 16.0.1 - 0.5.7 - 0.x.x
|
||||
- QtCreator 16.0.0 - 0.5.2 - 0.5.6
|
||||
- QtCreator 15.0.1 - 0.4.8 - 0.5.1
|
||||
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
|
||||
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
|
||||
@ -253,6 +261,46 @@ Linked files provide persistent context throughout the conversation:
|
||||
- on Linux with KDE Plasma: Ctrl + Alt + Q
|
||||
- To insert the full suggestion, you can use the TAB key
|
||||
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
|
||||
- To call Quick Refactor dialog, select some code or place cursor and press
|
||||
- on Mac: Option + Command + R
|
||||
- on Windows: Ctrl + Alt + R
|
||||
- on Linux with KDE Plasma: Ctrl + Alt + R
|
||||
|
||||
## Ignoring Files
|
||||
QodeAssist supports the ability to ignore files in context using a .qodeassistignore file. This allows you to exclude specific files from the context during code completion and in the chat assistant, which is especially useful for large projects.
|
||||
|
||||
### How to Use .qodeassistignore
|
||||
- Create a .qodeassistignore file in the root directory of your project near CMakeLists.txt or pro.
|
||||
- Add patterns for files and directories that should be excluded from the context.
|
||||
- QodeAssist will automatically detect this file and apply the exclusion rules.
|
||||
|
||||
### .qodeassistignore File Format
|
||||
The file format is similar to .gitignore:
|
||||
- Each pattern is written on a separate line
|
||||
- Empty lines are ignored
|
||||
- Lines starting with # are considered comments
|
||||
- Standard wildcards work the same as in .gitignore
|
||||
- To negate a pattern, use ! at the beginning of the line
|
||||
```
|
||||
# Ignore all files in the build directory
|
||||
build/
|
||||
|
||||
# Ignore all temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Ignore all files with .log extension
|
||||
*.log
|
||||
|
||||
# Ignore a specific file
|
||||
src/generated/autogen.cpp
|
||||
|
||||
# Ignore nested directories
|
||||
**/node_modules/
|
||||
|
||||
# Negation - DO NOT ignore this file
|
||||
!src/important.cpp
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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};
|
||||
|
||||
|
||||
@ -101,6 +101,8 @@ const char OPEN_AI_API_KEY[] = "QodeAssist.openAiApiKey";
|
||||
const char OPEN_AI_API_KEY_HISTORY[] = "QodeAssist.openAiApiKeyHistory";
|
||||
const char MISTRAL_AI_API_KEY[] = "QodeAssist.mistralAiApiKey";
|
||||
const char MISTRAL_AI_API_KEY_HISTORY[] = "QodeAssist.mistralAiApiKeyHistory";
|
||||
const char CODESTRAL_API_KEY[] = "QodeAssist.codestralApiKey";
|
||||
const char CODESTRAL_API_KEY_HISTORY[] = "QodeAssist.codestralApiKeyHistory";
|
||||
const char GOOGLE_AI_API_KEY[] = "QodeAssist.googleAiApiKey";
|
||||
const char GOOGLE_AI_API_KEY_HISTORY[] = "QodeAssist.googleAiApiKeyHistory";
|
||||
const char OLLAMA_BASIC_AUTH_API_KEY[] = "QodeAssist.ollamaBasicAuthApiKey";
|
||||
@ -149,5 +151,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
|
||||
|
||||
@ -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
|
||||
Reference in New Issue
Block a user