refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore

This commit is contained in:
Petr Mironychev
2026-07-14 02:50:43 +02:00
parent adef7972ed
commit 34d4f2c517
348 changed files with 809 additions and 852 deletions

View File

@@ -0,0 +1,95 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "AddCustomInstructionDialog.hpp"
#include "plugin/QodeAssisttr.h"
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QVBoxLayout>
namespace QodeAssist {
AddCustomInstructionDialog::AddCustomInstructionDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(Tr::tr("Add Custom Instruction"));
setupUi();
resize(500, 400);
}
AddCustomInstructionDialog::AddCustomInstructionDialog(const CustomInstruction &instruction, QWidget *parent)
: QDialog(parent)
, m_instruction(instruction)
{
setWindowTitle(Tr::tr("Edit Custom Instruction"));
setupUi();
m_nameEdit->setText(instruction.name);
m_bodyEdit->setPlainText(instruction.body);
m_defaultCheckBox->setChecked(instruction.isDefault);
resize(500, 400);
}
void AddCustomInstructionDialog::setupUi()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(10, 10, 10, 10);
mainLayout->setSpacing(10);
QFormLayout *formLayout = new QFormLayout();
m_nameEdit = new QLineEdit(this);
m_nameEdit->setPlaceholderText(Tr::tr("Enter instruction name..."));
formLayout->addRow(Tr::tr("Name:"), m_nameEdit);
mainLayout->addLayout(formLayout);
QLabel *bodyLabel = new QLabel(Tr::tr("Instruction Body:"), this);
mainLayout->addWidget(bodyLabel);
m_bodyEdit = new QPlainTextEdit(this);
m_bodyEdit->setPlaceholderText(
Tr::tr("Enter the refactoring instruction that will be sent to the LLM..."));
mainLayout->addWidget(m_bodyEdit);
m_defaultCheckBox = new QCheckBox(Tr::tr("Set as default instruction"), this);
m_defaultCheckBox->setToolTip(
Tr::tr("This instruction will be automatically selected when opening Quick Refactor dialog"));
mainLayout->addWidget(m_defaultCheckBox);
QDialogButtonBox *buttonBox
= new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, [this]() {
if (m_nameEdit->text().trimmed().isEmpty()) {
QMessageBox::warning(this, Tr::tr("Invalid Input"), Tr::tr("Instruction name cannot be empty."));
return;
}
if (m_bodyEdit->toPlainText().trimmed().isEmpty()) {
QMessageBox::warning(this, Tr::tr("Invalid Input"), Tr::tr("Instruction body cannot be empty."));
return;
}
accept();
});
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
}
CustomInstruction AddCustomInstructionDialog::getInstruction() const
{
CustomInstruction instruction = m_instruction;
instruction.name = m_nameEdit->text().trimmed();
instruction.body = m_bodyEdit->toPlainText().trimmed();
instruction.isDefault = m_defaultCheckBox->isChecked();
return instruction;
}
} // namespace QodeAssist

View File

@@ -0,0 +1,39 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QDialog>
#include <QString>
#include "CustomInstructionsManager.hpp"
class QLineEdit;
class QPlainTextEdit;
class QCheckBox;
namespace QodeAssist {
class AddCustomInstructionDialog : public QDialog
{
Q_OBJECT
public:
explicit AddCustomInstructionDialog(QWidget *parent = nullptr);
explicit AddCustomInstructionDialog(const CustomInstruction &instruction, QWidget *parent = nullptr);
~AddCustomInstructionDialog() override = default;
CustomInstruction getInstruction() const;
private:
void setupUi();
QLineEdit *m_nameEdit;
QPlainTextEdit *m_bodyEdit;
QCheckBox *m_defaultCheckBox;
CustomInstruction m_instruction;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,85 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CompletionErrorHandler.hpp"
#include <texteditor/texteditor.h>
#include <utils/tooltip/tooltip.h>
#include "ErrorWidget.hpp"
namespace QodeAssist {
void CompletionErrorHandler::showError(
TextEditor::TextEditorWidget *widget,
const QString &errorMessage,
int autoHideMs)
{
m_widget = widget;
m_errorMessage = errorMessage;
if (m_widget) {
const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor());
m_errorPosition = m_widget->viewport()->mapToGlobal(cursorRect.topLeft())
- Utils::ToolTip::offsetFromPosition();
identifyMatch(m_widget, m_widget->textCursor().position(), [this, autoHideMs](auto priority) {
if (priority != Priority_None) {
if (m_errorWidget) {
m_errorWidget->deleteLater();
}
m_errorWidget = new ErrorWidget(m_errorMessage, m_widget);
const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor());
QPoint globalPos = m_widget->viewport()->mapToGlobal(cursorRect.topLeft());
QPoint localPos = m_widget->mapFromGlobal(globalPos);
localPos.ry() -= m_errorWidget->height() + 5;
if (localPos.y() < 0) {
localPos.ry() = cursorRect.bottom() + 5;
}
m_errorWidget->move(localPos);
m_errorWidget->show();
m_errorWidget->raise();
QObject::connect(m_errorWidget, &ErrorWidget::dismissed, m_errorWidget, [this]() {
hideError();
});
}
});
}
}
void CompletionErrorHandler::hideError()
{
if (m_errorWidget) {
m_errorWidget->deleteLater();
m_errorWidget = nullptr;
}
Utils::ToolTip::hideImmediately();
m_errorMessage.clear();
}
void CompletionErrorHandler::identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
{
if (!editorWidget || m_errorMessage.isEmpty()) {
report(Priority_None);
return;
}
report(Priority_Tooltip);
}
void CompletionErrorHandler::operateTooltip(
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
{
Q_UNUSED(editorWidget)
Q_UNUSED(point)
}
} // namespace QodeAssist

View File

@@ -0,0 +1,39 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <texteditor/basehoverhandler.h>
#include <QPointer>
namespace QodeAssist {
class ErrorWidget;
class CompletionErrorHandler : public TextEditor::BaseHoverHandler
{
public:
void showError(
TextEditor::TextEditorWidget *widget,
const QString &errorMessage,
int autoHideMs = 5000);
void hideError();
bool isErrorVisible() const { return !m_errorWidget.isNull(); }
protected:
void identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override;
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
private:
QPointer<TextEditor::TextEditorWidget> m_widget;
QPointer<ErrorWidget> m_errorWidget;
QString m_errorMessage;
QPoint m_errorPosition;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,57 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CompletionHintHandler.hpp"
#include "CompletionHintWidget.hpp"
#include <texteditor/texteditor.h>
namespace QodeAssist {
CompletionHintHandler::CompletionHintHandler() = default;
CompletionHintHandler::~CompletionHintHandler()
{
hideHint();
}
void CompletionHintHandler::showHint(TextEditor::TextEditorWidget *widget, QPoint position, int fontSize)
{
if (!widget) {
return;
}
if (!m_hintWidget) {
m_hintWidget = new CompletionHintWidget(widget, fontSize);
}
m_hintWidget->move(position);
m_hintWidget->show();
m_hintWidget->raise();
}
void CompletionHintHandler::hideHint()
{
if (m_hintWidget) {
m_hintWidget->deleteLater();
m_hintWidget = nullptr;
}
}
bool CompletionHintHandler::isHintVisible() const
{
return !m_hintWidget.isNull() && m_hintWidget->isVisible();
}
void CompletionHintHandler::updateHintPosition(TextEditor::TextEditorWidget *widget, QPoint position)
{
if (!widget || !m_hintWidget) {
return;
}
m_hintWidget->move(position);
}
} // namespace QodeAssist

View File

@@ -0,0 +1,34 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QPointer>
#include <functional>
namespace TextEditor {
class TextEditorWidget;
}
namespace QodeAssist {
class CompletionHintWidget;
class CompletionHintHandler
{
public:
CompletionHintHandler();
~CompletionHintHandler();
void showHint(TextEditor::TextEditorWidget *widget, QPoint position, int fontSize);
void hideHint();
bool isHintVisible() const;
void updateHintPosition(TextEditor::TextEditorWidget *widget, QPoint position);
private:
QPointer<CompletionHintWidget> m_hintWidget;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,68 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CompletionHintWidget.hpp"
#include <QPainter>
#include <utils/theme/theme.h>
namespace QodeAssist {
CompletionHintWidget::CompletionHintWidget(QWidget *parent, int fontSize)
: QWidget(parent)
, m_isHovered(false)
{
m_accentColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
setMouseTracking(true);
setFocusPolicy(Qt::NoFocus);
setAttribute(Qt::WA_TranslucentBackground);
int triangleSize = qMax(6, fontSize / 2);
setFixedSize(triangleSize, triangleSize);
}
void CompletionHintWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QColor triangleColor = m_accentColor;
triangleColor.setAlpha(m_isHovered ? 255 : 200);
painter.setPen(Qt::NoPen);
painter.setBrush(triangleColor);
QPolygonF triangle;
int w = width();
int h = height();
triangle << QPointF(0, 0)
<< QPointF(0, h)
<< QPointF(w, h / 2.0);
painter.drawPolygon(triangle);
}
void CompletionHintWidget::enterEvent(QEnterEvent *event)
{
Q_UNUSED(event);
m_isHovered = true;
setCursor(Qt::PointingHandCursor);
update();
}
void CompletionHintWidget::leaveEvent(QEvent *event)
{
Q_UNUSED(event);
m_isHovered = false;
setCursor(Qt::ArrowCursor);
update();
}
} // namespace QodeAssist

View File

@@ -0,0 +1,29 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QWidget>
namespace QodeAssist {
class CompletionHintWidget : public QWidget
{
Q_OBJECT
public:
explicit CompletionHintWidget(QWidget *parent = nullptr, int fontSize = 12);
~CompletionHintWidget() override = default;
protected:
void paintEvent(QPaintEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
private:
QColor m_accentColor;
bool m_isHovered;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,96 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CompletionProgressHandler.hpp"
#include <QGraphicsBlurEffect>
#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
#include <QLabel>
#include <QPainter>
#include <QVBoxLayout>
#include <texteditor/texteditor.h>
#include <utils/progressindicator.h>
#include <utils/theme/theme.h>
#include <utils/tooltip/tooltip.h>
#include "ProgressWidget.hpp"
namespace QodeAssist {
void CompletionProgressHandler::showProgress(TextEditor::TextEditorWidget *widget)
{
m_widget = widget;
if (m_widget) {
const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor());
m_iconPosition = m_widget->viewport()->mapToGlobal(cursorRect.topLeft())
- Utils::ToolTip::offsetFromPosition();
identifyMatch(m_widget, m_widget->textCursor().position(), [this](auto priority) {
if (priority != Priority_None)
operateTooltip(m_widget, m_iconPosition);
});
}
}
void CompletionProgressHandler::hideProgress()
{
if (m_progressWidget) {
m_progressWidget->deleteLater();
m_progressWidget = nullptr;
}
Utils::ToolTip::hideImmediately();
}
void CompletionProgressHandler::setCancelCallback(std::function<void()> callback)
{
m_cancelCallback = callback;
}
void CompletionProgressHandler::identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
{
if (!editorWidget) {
report(Priority_None);
return;
}
report(Priority_Tooltip);
}
void CompletionProgressHandler::operateTooltip(
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
{
if (!editorWidget)
return;
if (m_progressWidget) {
delete m_progressWidget;
}
m_progressWidget = new ProgressWidget(editorWidget);
if (m_cancelCallback) {
m_progressWidget->setCancelCallback(m_cancelCallback);
}
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());
QPoint globalPos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft());
QPoint localPos = editorWidget->mapFromGlobal(globalPos);
localPos.rx() += 5;
localPos.ry() -= m_progressWidget->height() + 5;
if (localPos.y() < 0) {
localPos.ry() = cursorRect.bottom() + 5;
}
m_progressWidget->move(localPos);
m_progressWidget->show();
m_progressWidget->raise();
}
} // namespace QodeAssist

View File

@@ -0,0 +1,35 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <texteditor/basehoverhandler.h>
#include <QPointer>
#include <functional>
namespace QodeAssist {
class ProgressWidget;
class CompletionProgressHandler : public TextEditor::BaseHoverHandler
{
public:
void showProgress(TextEditor::TextEditorWidget *widget);
void hideProgress();
void setCancelCallback(std::function<void()> callback);
bool isProgressVisible() const { return !m_progressWidget.isNull(); }
protected:
void identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override;
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
private:
QPointer<TextEditor::TextEditorWidget> m_widget;
QPointer<ProgressWidget> m_progressWidget;
QPoint m_iconPosition;
std::function<void()> m_cancelCallback;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,110 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QString>
#include <QStringList>
#include <QTextBlock>
#include <QTextDocument>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
namespace QodeAssist {
class ContextExtractor
{
public:
static QString extractBefore(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
int lineCount)
{
if (!editor || lineCount <= 0) {
return QString();
}
QTextDocument *doc = editor->document();
int startLine = range.begin.line;
int contextStartLine = qMax(1, startLine - lineCount);
QStringList contextLines;
for (int line = contextStartLine; line < startLine; ++line) {
QTextBlock block = doc->findBlockByNumber(line - 1);
if (block.isValid()) {
contextLines.append(block.text());
}
}
return contextLines.join('\n');
}
static QString extractAfter(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
int lineCount)
{
if (!editor || lineCount <= 0) {
return QString();
}
QTextDocument *doc = editor->document();
int endLine = range.end.line;
int totalLines = doc->blockCount();
int contextEndLine = qMin(totalLines, endLine + lineCount);
QStringList contextLines;
for (int line = endLine + 1; line <= contextEndLine; ++line) {
QTextBlock block = doc->findBlockByNumber(line - 1);
if (block.isValid()) {
contextLines.append(block.text());
}
}
return contextLines.join('\n');
}
static QString extractLineContext(QTextDocument *doc, int position, bool before)
{
QTextCursor cursor(doc);
cursor.setPosition(position);
if (before) {
int posInBlock = cursor.positionInBlock();
cursor.movePosition(QTextCursor::StartOfBlock);
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, posInBlock);
return cursor.selectedText();
} else {
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
return cursor.selectedText();
}
}
static QStringList extractSurroundingLines(QTextDocument *doc, int position, int linesBefore, int linesAfter)
{
QTextCursor cursor(doc);
cursor.setPosition(position);
QTextBlock currentBlock = cursor.block();
QStringList result;
QTextBlock blockBefore = currentBlock.previous();
QStringList beforeLines;
for (int i = 0; i < linesBefore && blockBefore.isValid(); ++i) {
beforeLines.prepend(blockBefore.text());
blockBefore = blockBefore.previous();
}
result.append(beforeLines);
QTextBlock blockAfter = currentBlock.next();
for (int i = 0; i < linesAfter && blockAfter.isValid(); ++i) {
result.append(blockAfter.text());
blockAfter = blockAfter.next();
}
return result;
}
};
} // namespace QodeAssist

View File

@@ -0,0 +1,239 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CustomInstructionsManager.hpp"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <coreplugin/icore.h>
#include <logger/Logger.hpp>
namespace QodeAssist {
CustomInstructionsManager::CustomInstructionsManager(QObject *parent)
: QObject(parent)
{}
CustomInstructionsManager &CustomInstructionsManager::instance()
{
static CustomInstructionsManager instance;
return instance;
}
QString CustomInstructionsManager::getInstructionsDirectory() const
{
QString path = QString("%1/qodeassist/quick_refactor/instructions")
.arg(Core::ICore::userResourcePath().toFSPathString());
return path;
}
bool CustomInstructionsManager::ensureDirectoryExists() const
{
QDir dir(getInstructionsDirectory());
if (!dir.exists()) {
return dir.mkpath(".");
}
return true;
}
bool CustomInstructionsManager::loadInstructions()
{
m_instructions.clear();
if (!ensureDirectoryExists()) {
LOG_MESSAGE("Failed to create instructions directory");
return false;
}
QDir dir(getInstructionsDirectory());
QStringList filters;
filters << "*.json";
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
for (const QFileInfo &fileInfo : files) {
QFile file(fileInfo.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open instruction file: %1").arg(fileInfo.fileName()));
continue;
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
LOG_MESSAGE(
QString("Failed to parse instruction file %1: %2")
.arg(fileInfo.fileName(), error.errorString()));
continue;
}
QJsonObject obj = doc.object();
CustomInstruction instruction;
instruction.id = obj["id"].toString();
instruction.name = obj["name"].toString();
instruction.body = obj["body"].toString();
instruction.isDefault = obj["default"].toBool(false);
if (instruction.id.isEmpty() || instruction.name.isEmpty()) {
LOG_MESSAGE(QString("Invalid instruction in file: %1").arg(fileInfo.fileName()));
continue;
}
m_instructions.append(instruction);
}
LOG_MESSAGE(QString("Loaded %1 custom instructions").arg(m_instructions.size()));
return true;
}
bool CustomInstructionsManager::saveInstruction(const CustomInstruction &instruction)
{
if (!ensureDirectoryExists()) {
LOG_MESSAGE("Failed to create instructions directory");
return false;
}
CustomInstruction newInstruction = instruction;
QString oldFileName;
if (newInstruction.id.isEmpty()) {
newInstruction.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
} else {
// Check if instruction with this ID already exists and get old file name
for (int i = 0; i < m_instructions.size(); ++i) {
if (m_instructions[i].id == newInstruction.id) {
// Build old filename to delete it if name changed
QString oldName = m_instructions[i].name;
oldName.replace(' ', '_');
oldFileName = QString("%1/%2_%3.json")
.arg(getInstructionsDirectory(), oldName, newInstruction.id);
break;
}
}
}
// If this instruction is marked as default, remove default flag from all others
if (newInstruction.isDefault) {
for (int i = 0; i < m_instructions.size(); ++i) {
if (m_instructions[i].id != newInstruction.id && m_instructions[i].isDefault) {
m_instructions[i].isDefault = false;
// Update the file for this instruction
QString sanitizedName = m_instructions[i].name;
sanitizedName.replace(' ', '_');
QString otherFileName = QString("%1/%2_%3.json")
.arg(getInstructionsDirectory(), sanitizedName, m_instructions[i].id);
QJsonObject otherObj;
otherObj["id"] = m_instructions[i].id;
otherObj["name"] = m_instructions[i].name;
otherObj["body"] = m_instructions[i].body;
otherObj["default"] = false;
otherObj["version"] = "0.1";
QFile otherFile(otherFileName);
if (otherFile.open(QIODevice::WriteOnly)) {
otherFile.write(QJsonDocument(otherObj).toJson(QJsonDocument::Indented));
}
}
}
}
int existingIndex = -1;
for (int i = 0; i < m_instructions.size(); ++i) {
if (m_instructions[i].id == newInstruction.id) {
existingIndex = i;
break;
}
}
QJsonObject obj;
obj["id"] = newInstruction.id;
obj["name"] = newInstruction.name;
obj["body"] = newInstruction.body;
obj["default"] = newInstruction.isDefault;
obj["version"] = "0.1";
QJsonDocument doc(obj);
QString sanitizedName = newInstruction.name;
sanitizedName.replace(' ', '_');
QString fileName = QString("%1/%2_%3.json")
.arg(getInstructionsDirectory(), sanitizedName, newInstruction.id);
if (!oldFileName.isEmpty() && oldFileName != fileName) {
QFile::remove(oldFileName);
}
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
LOG_MESSAGE(QString("Failed to save instruction to file: %1").arg(fileName));
return false;
}
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
LOG_MESSAGE(QString("Failed to write instruction data: %1").arg(file.errorString()));
return false;
}
if (existingIndex >= 0) {
m_instructions[existingIndex] = newInstruction;
} else {
m_instructions.append(newInstruction);
}
emit instructionsChanged();
LOG_MESSAGE(QString("Saved custom instruction: %1").arg(newInstruction.name));
return true;
}
bool CustomInstructionsManager::deleteInstruction(const QString &id)
{
int index = -1;
for (int i = 0; i < m_instructions.size(); ++i) {
if (m_instructions[i].id == id) {
index = i;
break;
}
}
if (index < 0) {
LOG_MESSAGE(QString("Instruction not found: %1").arg(id));
return false;
}
QString sanitizedName = m_instructions[index].name;
sanitizedName.replace(' ', '_');
QString fileName = QString("%1/%2_%3.json")
.arg(getInstructionsDirectory(), sanitizedName, id);
QFile file(fileName);
if (!file.remove()) {
LOG_MESSAGE(QString("Failed to delete instruction file: %1").arg(fileName));
return false;
}
m_instructions.removeAt(index);
emit instructionsChanged();
LOG_MESSAGE(QString("Deleted custom instruction with id: %1").arg(id));
return true;
}
CustomInstruction CustomInstructionsManager::getInstructionById(const QString &id) const
{
for (const CustomInstruction &instruction : m_instructions) {
if (instruction.id == id) {
return instruction;
}
}
return CustomInstruction();
}
} // namespace QodeAssist

View File

@@ -0,0 +1,49 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QString>
#include <QVector>
namespace QodeAssist {
struct CustomInstruction
{
QString id;
QString name;
QString body;
bool isDefault = false;
};
class CustomInstructionsManager : public QObject
{
Q_OBJECT
public:
static CustomInstructionsManager &instance();
bool loadInstructions();
bool saveInstruction(const CustomInstruction &instruction);
bool deleteInstruction(const QString &id);
QVector<CustomInstruction> instructions() const { return m_instructions; }
CustomInstruction getInstructionById(const QString &id) const;
signals:
void instructionsChanged();
private:
explicit CustomInstructionsManager(QObject *parent = nullptr);
~CustomInstructionsManager() override = default;
QString getInstructionsDirectory() const;
bool ensureDirectoryExists() const;
QVector<CustomInstruction> m_instructions;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,56 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QString>
#include <QCoreApplication>
#include <utils/differ.h>
namespace QodeAssist {
class DiffStatistics
{
Q_DECLARE_TR_FUNCTIONS(DiffStatistics)
public:
DiffStatistics() = default;
void calculate(const QList<Utils::Diff> &diffList)
{
m_linesAdded = 0;
m_linesRemoved = 0;
for (const auto &diff : diffList) {
if (diff.command == Utils::Diff::Insert) {
m_linesAdded += diff.text.count('\n') + (diff.text.isEmpty() ? 0 : 1);
} else if (diff.command == Utils::Diff::Delete) {
m_linesRemoved += diff.text.count('\n') + (diff.text.isEmpty() ? 0 : 1);
}
}
}
int linesAdded() const { return m_linesAdded; }
int linesRemoved() const { return m_linesRemoved; }
QString formatSummary() const
{
if (m_linesAdded > 0 && m_linesRemoved > 0) {
return tr("+%1 lines, -%2 lines").arg(m_linesAdded).arg(m_linesRemoved);
} else if (m_linesAdded > 0) {
return tr("+%1 lines").arg(m_linesAdded);
} else if (m_linesRemoved > 0) {
return tr("-%1 lines").arg(m_linesRemoved);
}
return tr("No changes");
}
private:
int m_linesAdded{0};
int m_linesRemoved{0};
};
} // namespace QodeAssist

View File

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

View File

@@ -0,0 +1,38 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QPushButton>
#include <QTimer>
#include <QWidget>
namespace QodeAssist {
class EditorChatButton : public QWidget
{
Q_OBJECT
public:
explicit EditorChatButton(QWidget *parent = nullptr);
~EditorChatButton() override;
signals:
void clicked();
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
private:
QPixmap m_logoPixmap;
QColor m_textColor;
QColor m_backgroundColor;
bool m_isPressed = false;
bool m_isHovered = false;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,74 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "EditorChatButtonHandler.hpp"
#include "EditorChatButton.hpp"
#include <texteditor/texteditor.h>
#include <utils/tooltip/tooltip.h>
#include <QPoint>
namespace QodeAssist {
EditorChatButtonHandler::~EditorChatButtonHandler()
{
delete m_chatButton;
}
void EditorChatButtonHandler::showButton(TextEditor::TextEditorWidget *widget)
{
if (!widget)
return;
m_widget = widget;
identifyMatch(widget, widget->textCursor().position(), [this](auto priority) {
if (priority != Priority_None && m_widget) {
const QTextCursor cursor = m_widget->textCursor();
const QRect selectionRect = m_widget->cursorRect(cursor);
m_cursorPosition = m_widget->viewport()->mapToGlobal(selectionRect.topLeft())
- Utils::ToolTip::offsetFromPosition();
operateTooltip(m_widget, m_cursorPosition);
}
});
}
void EditorChatButtonHandler::hideButton()
{
Utils::ToolTip::hide();
}
void EditorChatButtonHandler::identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
{
if (!editorWidget) {
report(Priority_None);
return;
}
report(Priority_Tooltip);
}
void EditorChatButtonHandler::operateTooltip(
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
{
if (!editorWidget)
return;
if (!Utils::ToolTip::isVisible()) {
m_chatButton = new EditorChatButton(editorWidget);
m_buttonHeight = m_chatButton->height();
QPoint showPoint = point;
showPoint.ry() -= m_buttonHeight;
Utils::ToolTip::show(showPoint, m_chatButton, editorWidget);
} else {
QPoint showPoint = point;
showPoint.ry() -= m_buttonHeight;
Utils::ToolTip::move(showPoint);
}
}
} // namespace QodeAssist

View File

@@ -0,0 +1,37 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include "widgets/EditorChatButton.hpp"
#include <texteditor/basehoverhandler.h>
#include <QPointer>
namespace QodeAssist {
class EditorChatButtonHandler : public TextEditor::BaseHoverHandler
{
public:
explicit EditorChatButtonHandler() = default;
~EditorChatButtonHandler() override;
void showButton(TextEditor::TextEditorWidget *widget);
void hideButton();
signals:
void chatButtonClicked(TextEditor::TextEditorWidget *widget);
protected:
void identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override;
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
private:
QPointer<TextEditor::TextEditorWidget> m_widget;
QPoint m_cursorPosition;
EditorChatButton *m_chatButton = nullptr;
int m_buttonHeight = 0;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,198 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ErrorWidget.hpp"
#include <QFontMetrics>
#include <QMouseEvent>
#include <QPainterPath>
namespace QodeAssist {
ErrorWidget::ErrorWidget(const QString &errorMessage, QWidget *parent, int autoHideMs)
: QWidget(parent)
, m_errorMessage(errorMessage)
, m_autoHideTimer(nullptr)
, m_isHovered(false)
{
setupColors();
setupIcon();
QFont errorFont = font();
errorFont.setPointSize(qMax(8, errorFont.pointSize() - 2));
setFont(errorFont);
setFixedSize(calculateSize());
setAttribute(Qt::WA_TranslucentBackground);
setMouseTracking(true);
if (autoHideMs > 0) {
m_autoHideTimer = new QTimer(this);
m_autoHideTimer->setSingleShot(true);
connect(m_autoHideTimer, &QTimer::timeout, this, [this]() {
if (!m_isHovered) {
emit dismissed();
deleteLater();
}
});
m_autoHideTimer->start(autoHideMs);
}
}
ErrorWidget::~ErrorWidget()
{
if (m_autoHideTimer) {
m_autoHideTimer->stop();
}
}
void ErrorWidget::setErrorMessage(const QString &message)
{
m_errorMessage = message;
QFont smallFont = font();
smallFont.setPointSize(qMax(8, smallFont.pointSize() - 2));
setFont(smallFont);
setFixedSize(calculateSize());
update();
}
void ErrorWidget::setupColors()
{
m_textColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
m_backgroundColor = Utils::creatorTheme()->color(Utils::Theme::BackgroundColorNormal);
m_errorColor = Utils::creatorTheme()->color(Utils::Theme::TextColorError);
}
void ErrorWidget::setupIcon()
{
// Create a smaller error icon (exclamation mark in a circle)
QPixmap pixmap(18, 18);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
// Draw circle
painter.setPen(QPen(m_errorColor, 1.5));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(1, 1, 16, 16);
// Draw exclamation mark
painter.setPen(Qt::NoPen);
painter.setBrush(m_errorColor);
// Vertical bar of exclamation
painter.drawRect(8, 4, 2, 8);
// Dot of exclamation
painter.drawRect(8, 13, 2, 2);
m_errorIcon = pixmap;
}
QSize ErrorWidget::calculateSize() const
{
QFontMetrics fm(font());
// Maximum width for the text area
const int maxTextWidth = 350;
const int iconWidth = 18;
const int padding = 8;
const int margin = 12;
// Calculate text area with word wrapping
QRect textRect = fm.boundingRect(
0, 0, maxTextWidth, 1000,
Qt::AlignLeft | Qt::TextWordWrap,
m_errorMessage
);
// Total width: margin + icon + padding + text + margin
int totalWidth = margin + iconWidth + padding + textRect.width() + margin;
// Total height: larger of icon or text, plus vertical margins
int contentHeight = qMax(iconWidth, textRect.height());
int totalHeight = contentHeight + margin * 2;
return QSize(totalWidth, totalHeight);
}
void ErrorWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
// Draw background with border
QColor bgColor = m_backgroundColor;
if (m_isHovered) {
bgColor = bgColor.lighter(110);
}
QPainterPath path;
path.addRoundedRect(rect().adjusted(1, 1, -1, -1), 4, 4);
painter.fillPath(path, bgColor);
painter.setPen(QPen(m_errorColor.lighter(150), 1));
painter.drawPath(path);
const int iconSize = 18;
const int padding = 8;
const int margin = 12;
// Draw error icon
if (!m_errorIcon.isNull()) {
QRect iconRect(margin, margin, iconSize, iconSize);
painter.drawPixmap(iconRect, m_errorIcon);
}
// Draw error message with word wrap
painter.setPen(m_textColor);
QRect textRect = rect().adjusted(
margin + iconSize + padding, // left
margin, // top
-margin, // right
-margin // bottom
);
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, m_errorMessage);
}
void ErrorWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
emit dismissed();
deleteLater();
}
QWidget::mousePressEvent(event);
}
void ErrorWidget::enterEvent(QEnterEvent *event)
{
m_isHovered = true;
update();
// Stop auto-hide timer while hovering
if (m_autoHideTimer && m_autoHideTimer->isActive()) {
m_autoHideTimer->stop();
}
QWidget::enterEvent(event);
}
void ErrorWidget::leaveEvent(QEvent *event)
{
m_isHovered = false;
update();
// Restart auto-hide timer when leaving
if (m_autoHideTimer) {
m_autoHideTimer->start(2000); // Give 2 more seconds after leaving
}
QWidget::leaveEvent(event);
}
} // namespace QodeAssist

View File

@@ -0,0 +1,50 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QPainter>
#include <QTimer>
#include <QWidget>
#include <utils/theme/theme.h>
namespace QodeAssist {
class ErrorWidget : public QWidget
{
Q_OBJECT
public:
explicit ErrorWidget(const QString &errorMessage, QWidget *parent = nullptr, int autoHideMs = 5000);
~ErrorWidget();
void setErrorMessage(const QString &message);
QString errorMessage() const { return m_errorMessage; }
signals:
void dismissed();
protected:
void paintEvent(QPaintEvent *) override;
void mousePressEvent(QMouseEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
private:
QString m_errorMessage;
QTimer *m_autoHideTimer;
QColor m_textColor;
QColor m_backgroundColor;
QColor m_errorColor;
QPixmap m_errorIcon;
bool m_isHovered;
void setupColors();
void setupIcon();
QSize calculateSize() const;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,156 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ProgressWidget.hpp"
#include <QMouseEvent>
namespace QodeAssist {
ProgressWidget::ProgressWidget(QWidget *parent)
: QWidget(parent)
, m_isHovered(false)
{
m_dotPosition = 0;
m_timer.setInterval(300);
connect(&m_timer, &QTimer::timeout, this, [this]() {
m_dotPosition = (m_dotPosition + 1) % 4;
update();
});
m_timer.start();
m_textColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
m_backgroundColor = Utils::creatorTheme()->color(Utils::Theme::BackgroundColorNormal);
m_logoPixmap = QPixmap(":/resources/images/qoderassist-icon.png");
if (!m_logoPixmap.isNull()) {
QImage image = m_logoPixmap.toImage();
image = image.convertToFormat(QImage::Format_ARGB32);
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
QColor pixelColor = QColor::fromRgba(image.pixel(x, y));
int brightness = (pixelColor.red() + pixelColor.green() + pixelColor.blue()) / 3;
if (brightness > 200) {
pixelColor.setAlpha(0);
image.setPixelColor(x, y, pixelColor);
} else if (pixelColor.alpha() > 0) {
int alpha = pixelColor.alpha();
pixelColor = m_textColor;
pixelColor.setAlpha(alpha);
image.setPixelColor(x, y, pixelColor);
}
}
}
m_logoPixmap = QPixmap::fromImage(image);
m_logoPixmap = m_logoPixmap.scaled(24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
setFixedSize(40, 40);
setMouseTracking(true);
}
ProgressWidget::~ProgressWidget()
{
m_timer.stop();
}
void ProgressWidget::setCancelCallback(std::function<void()> callback)
{
m_cancelCallback = callback;
}
void ProgressWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.fillRect(rect(), m_backgroundColor);
if (!m_isHovered) {
if (!m_logoPixmap.isNull()) {
QRect logoRect(
(width() - m_logoPixmap.width()) / 2,
5,
m_logoPixmap.width(),
m_logoPixmap.height());
painter.drawPixmap(logoRect, m_logoPixmap);
}
int dotSpacing = 6;
int dotSize = 4;
int totalDotWidth = 3 * dotSize + 2 * dotSpacing;
int startX = (width() - totalDotWidth) / 2;
int dotY = height() - 8;
for (int i = 0; i < 3; ++i) {
QColor dotColor = m_textColor;
if (m_dotPosition == 0) {
dotColor.setAlpha(128);
} else {
if (i == m_dotPosition - 1) {
dotColor.setAlpha(255);
} else {
dotColor.setAlpha(80);
}
}
painter.setPen(Qt::NoPen);
painter.setBrush(dotColor);
int x = startX + i * (dotSize + dotSpacing);
painter.drawEllipse(x, dotY, dotSize, dotSize);
}
}
if (m_isHovered) {
int closeSize = 14;
int centerX = width() / 2;
int centerY = height() / 2;
QPen closePen(m_textColor, 2);
closePen.setCapStyle(Qt::RoundCap);
painter.setPen(closePen);
int offset = closeSize / 2;
painter.drawLine(centerX - offset, centerY - offset, centerX + offset, centerY + offset);
painter.drawLine(centerX + offset, centerY - offset, centerX - offset, centerY + offset);
}
}
void ProgressWidget::enterEvent(QEnterEvent *event)
{
Q_UNUSED(event);
m_isHovered = true;
setCursor(Qt::PointingHandCursor);
update();
}
void ProgressWidget::leaveEvent(QEvent *event)
{
Q_UNUSED(event);
m_isHovered = false;
setCursor(Qt::ArrowCursor);
update();
}
void ProgressWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && m_isHovered) {
event->accept();
auto callback = m_cancelCallback;
emit cancelRequested();
if (callback)
callback();
return;
}
QWidget::mousePressEvent(event);
}
} // namespace QodeAssist

View File

@@ -0,0 +1,45 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QPainter>
#include <QTimer>
#include <QWidget>
#include <functional>
#include <utils/progressindicator.h>
#include <utils/theme/theme.h>
namespace QodeAssist {
class ProgressWidget : public QWidget
{
Q_OBJECT
public:
ProgressWidget(QWidget *parent = nullptr);
~ProgressWidget();
void setCancelCallback(std::function<void()> callback);
signals:
void cancelRequested();
protected:
void paintEvent(QPaintEvent *) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private:
QTimer m_timer;
int m_dotPosition;
QColor m_textColor;
QColor m_backgroundColor;
QPixmap m_logoPixmap;
bool m_isHovered;
std::function<void()> m_cancelCallback;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,650 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "QuickRefactorDialog.hpp"
#include "AddCustomInstructionDialog.hpp"
#include "CustomInstructionsManager.hpp"
#include "plugin/QodeAssisttr.h"
#include "settings/ConfigurationManager.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "settings/SettingsConstants.hpp"
#include <QApplication>
#include <QComboBox>
#include <QCompleter>
#include <QDesktopServices>
#include <QDialogButtonBox>
#include <QDir>
#include <QFontMetrics>
#include <QFrame>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPainter>
#include <QPlainTextEdit>
#include <QScreen>
#include <QStringListModel>
#include <QSvgRenderer>
#include <QTimer>
#include <QToolButton>
#include <QUrl>
#include <QVBoxLayout>
#include <coreplugin/icore.h>
#include <utils/theme/theme.h>
#include <utils/utilsicons.h>
namespace QodeAssist {
static QIcon createThemedIcon(const QString &svgPath, const QColor &color)
{
QSvgRenderer renderer(svgPath);
if (!renderer.isValid()) {
return QIcon();
}
QSize iconSize(16, 16);
QPixmap pixmap(iconSize);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
renderer.render(&painter);
painter.end();
QImage image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
uchar *bits = image.bits();
const int bytesPerPixel = 4;
const int totalBytes = image.width() * image.height() * bytesPerPixel;
const int newR = color.red();
const int newG = color.green();
const int newB = color.blue();
for (int i = 0; i < totalBytes; i += bytesPerPixel) {
int alpha = bits[i + 3];
if (alpha > 0) {
bits[i] = newB;
bits[i + 1] = newG;
bits[i + 2] = newR;
}
}
return QIcon(QPixmap::fromImage(image));
}
QuickRefactorDialog::QuickRefactorDialog(QWidget *parent, const QString &lastInstructions)
: QDialog(parent)
, m_lastInstructions(lastInstructions)
{
setWindowTitle(Tr::tr("Quick Refactor"));
setupUi();
if (!m_lastInstructions.isEmpty()) {
m_instructionEdit->setPlainText(m_lastInstructions);
m_instructionEdit->selectAll();
}
QTimer::singleShot(0, this, &QuickRefactorDialog::updateDialogSize);
m_instructionEdit->installEventFilter(this);
m_commandsComboBox->installEventFilter(this);
updateDialogSize();
m_instructionEdit->setFocus();
}
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();
m_configComboBox = new QComboBox(this);
m_configComboBox->setMinimumWidth(200);
m_configComboBox->setToolTip(Tr::tr("Switch AI configuration"));
actionsLayout->addWidget(m_configComboBox);
Utils::Theme *theme = Utils::creatorTheme();
QColor iconColor = theme ? theme->color(Utils::Theme::TextColorNormal) : QColor(Qt::white);
m_toolsIconOn = createThemedIcon(":/qt/qml/ChatView/icons/tools-icon-on.svg", iconColor);
m_toolsIconOff = createThemedIcon(":/qt/qml/ChatView/icons/tools-icon-off.svg", iconColor);
m_toolsButton = new QToolButton(this);
m_toolsButton->setCheckable(true);
m_toolsButton->setChecked(Settings::quickRefactorSettings().useTools());
m_toolsButton->setIcon(m_toolsButton->isChecked() ? m_toolsIconOn : m_toolsIconOff);
m_toolsButton->setToolTip(Tr::tr("Enable/Disable AI Tools"));
m_toolsButton->setIconSize(QSize(16, 16));
actionsLayout->addWidget(m_toolsButton);
connect(m_toolsButton, &QToolButton::toggled, this, [this](bool checked) {
m_toolsButton->setIcon(checked ? m_toolsIconOn : m_toolsIconOff);
Settings::quickRefactorSettings().useTools.setValue(checked);
Settings::quickRefactorSettings().writeSettings();
});
m_thinkingIconOn = createThemedIcon(":/qt/qml/ChatView/icons/thinking-icon-on.svg", iconColor);
m_thinkingIconOff = createThemedIcon(":/qt/qml/ChatView/icons/thinking-icon-off.svg", iconColor);
m_thinkingButton = new QToolButton(this);
m_thinkingButton->setCheckable(true);
m_thinkingButton->setChecked(Settings::quickRefactorSettings().useThinking());
m_thinkingButton->setIcon(m_thinkingButton->isChecked() ? m_thinkingIconOn : m_thinkingIconOff);
m_thinkingButton->setToolTip(Tr::tr("Enable/Disable Thinking Mode"));
m_thinkingButton->setIconSize(QSize(16, 16));
actionsLayout->addWidget(m_thinkingButton);
connect(m_thinkingButton, &QToolButton::toggled, this, [this](bool checked) {
m_thinkingButton->setIcon(checked ? m_thinkingIconOn : m_thinkingIconOff);
Settings::quickRefactorSettings().useThinking.setValue(checked);
Settings::quickRefactorSettings().writeSettings();
});
m_settingsButton = new QToolButton(this);
m_settingsButton->setIcon(Utils::Icons::SETTINGS_TOOLBAR.icon());
m_settingsButton->setToolTip(Tr::tr("Open Quick Refactor Settings"));
m_settingsButton->setIconSize(QSize(16, 16));
actionsLayout->addWidget(m_settingsButton);
connect(m_settingsButton, &QToolButton::clicked, this, &QuickRefactorDialog::onOpenSettings);
mainLayout->addLayout(actionsLayout);
QLabel *instructionLabel = new QLabel(Tr::tr("Your Current Instruction:"), this);
mainLayout->addWidget(instructionLabel);
m_instructionEdit = new QPlainTextEdit(this);
m_instructionEdit->setMinimumHeight(80);
m_instructionEdit->setPlaceholderText(Tr::tr("Type or edit your instruction..."));
mainLayout->addWidget(m_instructionEdit);
QHBoxLayout *savedInstructionsLayout = new QHBoxLayout();
savedInstructionsLayout->setSpacing(4);
QLabel *savedLabel = new QLabel(Tr::tr("Or Load saved:"), this);
savedInstructionsLayout->addWidget(savedLabel);
m_commandsComboBox = new QComboBox(this);
m_commandsComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_commandsComboBox->setEditable(true);
m_commandsComboBox->setInsertPolicy(QComboBox::NoInsert);
m_commandsComboBox->lineEdit()->setPlaceholderText(Tr::tr("Search saved instructions..."));
QCompleter *completer = new QCompleter(this);
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
m_commandsComboBox->setCompleter(completer);
savedInstructionsLayout->addWidget(m_commandsComboBox);
m_addCommandButton = new QToolButton(this);
m_addCommandButton->setText("+");
m_addCommandButton->setToolTip(Tr::tr("Add Custom Instruction"));
m_addCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_addCommandButton);
m_editCommandButton = new QToolButton(this);
m_editCommandButton->setText("");
m_editCommandButton->setToolTip(Tr::tr("Edit Custom Instruction"));
m_editCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_editCommandButton);
m_deleteCommandButton = new QToolButton(this);
m_deleteCommandButton->setText("");
m_deleteCommandButton->setToolTip(Tr::tr("Delete Custom Instruction"));
m_deleteCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_deleteCommandButton);
m_openFolderButton = new QToolButton(this);
m_openFolderButton->setText("📁");
m_openFolderButton->setToolTip(Tr::tr("Open Instructions Folder"));
m_openFolderButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_openFolderButton);
mainLayout->addLayout(savedInstructionsLayout);
connect(
m_instructionEdit,
&QPlainTextEdit::textChanged,
this,
&QuickRefactorDialog::updateDialogSize);
connect(
m_commandsComboBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this,
&QuickRefactorDialog::onCommandSelected);
connect(m_addCommandButton, &QToolButton::clicked, this, &QuickRefactorDialog::onAddCustomCommand);
connect(
m_editCommandButton, &QToolButton::clicked, this, &QuickRefactorDialog::onEditCustomCommand);
connect(
m_deleteCommandButton,
&QToolButton::clicked,
this,
&QuickRefactorDialog::onDeleteCustomCommand);
connect(
m_openFolderButton,
&QToolButton::clicked,
this,
&QuickRefactorDialog::onOpenInstructionsFolder);
loadCustomCommands();
loadAvailableConfigurations();
connect(
m_configComboBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this,
&QuickRefactorDialog::onConfigurationChanged);
QDialogButtonBox *buttonBox
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QuickRefactorDialog::validateAndAccept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
QPushButton *cancelButton = buttonBox->button(QDialogButtonBox::Cancel);
if (okButton) {
okButton->installEventFilter(this);
}
if (cancelButton) {
cancelButton->installEventFilter(this);
}
setTabOrder(m_instructionEdit, m_commandsComboBox);
setTabOrder(m_commandsComboBox, okButton);
setTabOrder(okButton, cancelButton);
}
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_instructionEdit->toPlainText().trimmed();
}
void QuickRefactorDialog::setInstructions(const QString &instructions)
{
m_instructionEdit->setPlainText(instructions);
}
QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
{
return m_selectedAction;
}
void QuickRefactorDialog::keyPressEvent(QKeyEvent *event)
{
QDialog::keyPressEvent(event);
}
bool QuickRefactorDialog::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (watched == m_instructionEdit) {
if (keyEvent->key() == Qt::Key_Tab) {
m_commandsComboBox->setFocus();
return true;
}
}
if (watched == m_commandsComboBox || watched == m_commandsComboBox->lineEdit()) {
if (keyEvent->key() == Qt::Key_Tab) {
QPushButton *okButton = findChild<QPushButton *>();
if (okButton && okButton->text() == "OK") {
okButton->setFocus();
} else {
focusNextChild();
}
return true;
}
}
}
return QDialog::eventFilter(watched, event);
}
void QuickRefactorDialog::useLastInstructions()
{
if (!m_lastInstructions.isEmpty()) {
m_commandsComboBox->setCurrentIndex(0);
m_instructionEdit->setPlainText(m_lastInstructions);
m_selectedAction = Action::RepeatLast;
}
accept();
}
void QuickRefactorDialog::useImproveCodeTemplate()
{
m_commandsComboBox->setCurrentIndex(0);
m_instructionEdit->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_commandsComboBox->setCurrentIndex(0);
m_instructionEdit->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_instructionEdit->toPlainText();
QFontMetrics fm(m_instructionEdit->font());
QStringList lines = text.split('\n');
int lineCount = qMax(lines.size(), 3);
m_instructionEdit->setMaximumHeight(QWIDGETSIZE_MAX);
int lineHeight = fm.height() + 2;
int textEditHeight = qMin(qMax(lineCount, 3) * lineHeight, 15 * lineHeight);
m_instructionEdit->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 = qMin(m_instructionEdit->minimumHeight() + 200, screenGeometry.height() * 3 / 4);
resize(newWidth, newHeight);
}
void QuickRefactorDialog::loadCustomCommands()
{
m_commandsComboBox->clear();
m_commandsComboBox->addItem("", QString());
auto &manager = CustomInstructionsManager::instance();
const QVector<CustomInstruction> &instructions = manager.instructions();
QStringList instructionNames;
for (const CustomInstruction &instruction : instructions) {
m_commandsComboBox->addItem(instruction.name, instruction.id);
instructionNames.append(instruction.name);
}
if (m_commandsComboBox->completer()) {
QStringListModel *model = new QStringListModel(instructionNames, this);
m_commandsComboBox->completer()->setModel(model);
}
bool hasInstructions = !instructions.isEmpty();
m_editCommandButton->setEnabled(hasInstructions);
m_deleteCommandButton->setEnabled(hasInstructions);
}
CustomInstruction QuickRefactorDialog::findCurrentInstruction() const
{
QString currentText = m_commandsComboBox->currentText().trimmed();
if (currentText.isEmpty()) {
return CustomInstruction();
}
auto &manager = CustomInstructionsManager::instance();
const QVector<CustomInstruction> &instructions = manager.instructions();
for (const CustomInstruction &instruction : instructions) {
if (instruction.name == currentText) {
return instruction;
}
}
int currentIndex = m_commandsComboBox->currentIndex();
if (currentIndex > 0) {
QString instructionId = m_commandsComboBox->itemData(currentIndex).toString();
if (!instructionId.isEmpty()) {
return manager.getInstructionById(instructionId);
}
}
return CustomInstruction();
}
void QuickRefactorDialog::onCommandSelected(int index)
{
if (index <= 0) {
return;
}
CustomInstruction instruction = findCurrentInstruction();
if (!instruction.id.isEmpty()) {
m_instructionEdit->setPlainText(instruction.body);
}
}
void QuickRefactorDialog::onAddCustomCommand()
{
AddCustomInstructionDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
CustomInstruction instruction = dialog.getInstruction();
auto &manager = CustomInstructionsManager::instance();
if (manager.saveInstruction(instruction)) {
loadCustomCommands();
m_commandsComboBox->setCurrentText(instruction.name);
} else {
QMessageBox::warning(
this,
Tr::tr("Error"),
Tr::tr("Failed to save custom instruction. Check logs for details."));
}
}
}
void QuickRefactorDialog::onEditCustomCommand()
{
CustomInstruction instruction = findCurrentInstruction();
if (instruction.id.isEmpty()) {
QMessageBox::information(
this,
Tr::tr("No Instruction Selected"),
Tr::tr("Please select an instruction to edit."));
return;
}
AddCustomInstructionDialog dialog(instruction, this);
if (dialog.exec() == QDialog::Accepted) {
CustomInstruction updatedInstruction = dialog.getInstruction();
auto &manager = CustomInstructionsManager::instance();
if (manager.saveInstruction(updatedInstruction)) {
loadCustomCommands();
m_commandsComboBox->setCurrentText(updatedInstruction.name);
} else {
QMessageBox::warning(
this,
Tr::tr("Error"),
Tr::tr("Failed to update custom instruction. Check logs for details."));
}
}
}
void QuickRefactorDialog::onDeleteCustomCommand()
{
CustomInstruction instruction = findCurrentInstruction();
if (instruction.id.isEmpty()) {
QMessageBox::information(
this,
Tr::tr("No Instruction Selected"),
Tr::tr("Please select an instruction to delete."));
return;
}
QMessageBox::StandardButton reply = QMessageBox::question(
this,
Tr::tr("Confirm Deletion"),
Tr::tr("Are you sure you want to delete the instruction '%1'?").arg(instruction.name),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
auto &manager = CustomInstructionsManager::instance();
if (manager.deleteInstruction(instruction.id)) {
loadCustomCommands();
m_commandsComboBox->setCurrentIndex(0);
m_commandsComboBox->clearEditText();
} else {
QMessageBox::warning(
this,
Tr::tr("Error"),
Tr::tr("Failed to delete custom instruction. Check logs for details."));
}
}
}
void QuickRefactorDialog::onOpenInstructionsFolder()
{
QString path = QString("%1/qodeassist/quick_refactor/instructions")
.arg(Core::ICore::userResourcePath().toFSPathString());
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
void QuickRefactorDialog::onOpenSettings()
{
Settings::showSettings(Constants::QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID);
}
QString QuickRefactorDialog::selectedConfiguration() const
{
return m_selectedConfiguration;
}
void QuickRefactorDialog::loadAvailableConfigurations()
{
auto &manager = Settings::ConfigurationManager::instance();
manager.loadConfigurations(Settings::ConfigurationType::QuickRefactor);
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::QuickRefactor);
m_configComboBox->clear();
m_configComboBox->addItem(Tr::tr("Current"), QString());
for (const Settings::AIConfiguration &config : configs) {
m_configComboBox->addItem(config.name, config.id);
}
auto &settings = Settings::generalSettings();
QString currentProvider = settings.qrProvider.value();
QString currentModel = settings.qrModel.value();
QString currentConfigText = QString("%1/%2").arg(currentProvider, currentModel);
m_configComboBox->setItemText(0, Tr::tr("Current (%1)").arg(currentConfigText));
}
void QuickRefactorDialog::onConfigurationChanged(int index)
{
if (index == 0) {
m_selectedConfiguration.clear();
return;
}
QString configId = m_configComboBox->itemData(index).toString();
m_selectedConfiguration = m_configComboBox->itemText(index);
auto &manager = Settings::ConfigurationManager::instance();
Settings::AIConfiguration config
= manager.getConfigurationById(configId, Settings::ConfigurationType::QuickRefactor);
if (!config.id.isEmpty()) {
auto &settings = Settings::generalSettings();
settings.qrProvider.setValue(config.provider);
settings.qrModel.setValue(config.model);
settings.qrTemplate.setValue(config.templateName);
settings.qrUrl.setValue(config.url);
settings.qrCustomEndpoint.setValue(config.customEndpoint);
settings.writeSettings();
}
}
void QuickRefactorDialog::validateAndAccept()
{
QString instruction = m_instructionEdit->toPlainText().trimmed();
if (instruction.isEmpty()) {
QMessageBox::warning(
this,
Tr::tr("No Instruction"),
Tr::tr("Please type an instruction or select a saved one."));
m_instructionEdit->setFocus();
return;
}
accept();
}
} // namespace QodeAssist

View File

@@ -0,0 +1,86 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QDialog>
#include <QString>
#include "CustomInstructionsManager.hpp"
class QPlainTextEdit;
class QToolButton;
class QLabel;
class QComboBox;
class QLineEdit;
class QFrame;
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;
QString selectedConfiguration() const;
bool eventFilter(QObject *watched, QEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private slots:
void useLastInstructions();
void useImproveCodeTemplate();
void useAlternativeSolutionTemplate();
void updateDialogSize();
void onCommandSelected(int index);
void onAddCustomCommand();
void onEditCustomCommand();
void onDeleteCustomCommand();
void onOpenInstructionsFolder();
void onOpenSettings();
void loadCustomCommands();
void loadAvailableConfigurations();
void onConfigurationChanged(int index);
void validateAndAccept();
private:
void setupUi();
void createActionButtons();
CustomInstruction findCurrentInstruction() const;
QPlainTextEdit *m_instructionEdit;
QToolButton *m_repeatButton;
QToolButton *m_improveButton;
QToolButton *m_alternativeButton;
QToolButton *m_addCommandButton;
QToolButton *m_editCommandButton;
QToolButton *m_deleteCommandButton;
QToolButton *m_openFolderButton;
QToolButton *m_settingsButton;
QToolButton *m_toolsButton;
QToolButton *m_thinkingButton;
QComboBox *m_commandsComboBox;
QComboBox *m_configComboBox;
Action m_selectedAction = Action::Custom;
QString m_lastInstructions;
QString m_selectedConfiguration;
QIcon m_toolsIconOn;
QIcon m_toolsIconOff;
QIcon m_thinkingIconOn;
QIcon m_thinkingIconOff;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,747 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "RefactorWidget.hpp"
#include "DiffStatistics.hpp"
#include <texteditor/textdocument.h>
#include <texteditor/syntaxhighlighter.h>
#include <QCloseEvent>
#include <QEnterEvent>
#include <QEvent>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QPainter>
#include <QRegion>
#include <QScreen>
#include <QScrollBar>
#include <QSharedPointer>
#include <QSplitter>
#include <QTextBlock>
#include <QVBoxLayout>
#include <coreplugin/icore.h>
#include <utils/differ.h>
#include <utils/theme/theme.h>
#include "settings/QuickRefactorSettings.hpp"
namespace QodeAssist {
CustomSplitterHandle::CustomSplitterHandle(Qt::Orientation orientation, QSplitter *parent)
: QSplitterHandle(orientation, parent)
{
if (orientation == Qt::Horizontal) {
setCursor(Qt::SplitHCursor);
} else {
setCursor(Qt::SplitVCursor);
}
setMouseTracking(true);
}
void CustomSplitterHandle::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QColor bgColor = Utils::creatorColor(Utils::Theme::BackgroundColorHover);
bgColor.setAlpha(m_hovered ? 150 : 50);
painter.fillRect(rect(), bgColor);
QColor lineColor = Utils::creatorColor(Utils::Theme::SplitterColor);
lineColor.setAlpha(m_hovered ? 255 : 180);
const int lineWidth = m_hovered ? 3 : 2;
const int margin = 10;
painter.setPen(QPen(lineColor, lineWidth));
if (orientation() == Qt::Horizontal) {
int x = width() / 2;
painter.drawLine(x, margin, x, height() - margin);
painter.setBrush(lineColor);
int centerY = height() / 2;
const int dotSize = m_hovered ? 3 : 2;
const int dotSpacing = 8;
for (int i = -2; i <= 2; ++i) {
painter.drawEllipse(QPoint(x, centerY + i * dotSpacing), dotSize, dotSize);
}
} else {
int y = height() / 2;
painter.drawLine(margin, y, width() - margin, y);
}
}
void CustomSplitterHandle::enterEvent(QEnterEvent *event)
{
m_hovered = true;
update();
QSplitterHandle::enterEvent(event);
}
void CustomSplitterHandle::leaveEvent(QEvent *event)
{
m_hovered = false;
update();
QSplitterHandle::leaveEvent(event);
}
CustomSplitter::CustomSplitter(Qt::Orientation orientation, QWidget *parent)
: QSplitter(orientation, parent)
{
}
QSplitterHandle *CustomSplitter::createHandle()
{
return new CustomSplitterHandle(orientation(), this);
}
RefactorWidget::RefactorWidget(TextEditor::TextEditorWidget *sourceEditor, QWidget *parent)
: QWidget(parent)
, m_sourceEditor(sourceEditor)
, m_leftEditor(nullptr)
, m_rightEditor(nullptr)
, m_leftContainer(nullptr)
, m_splitter(nullptr)
, m_statsLabel(nullptr)
, m_applyButton(nullptr)
, m_declineButton(nullptr)
, m_editorWidth(800)
, m_syncingScroll(false)
, m_isClosing(false)
, m_linesAdded(0)
, m_linesRemoved(0)
{
setupUi();
applyEditorSettings();
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
setAttribute(Qt::WA_DeleteOnClose);
setFocusPolicy(Qt::StrongFocus);
}
RefactorWidget::~RefactorWidget()
{
}
void RefactorWidget::setupUi()
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(6, 6, 6, 6);
mainLayout->setSpacing(4);
m_statsLabel = new QLabel(this);
m_statsLabel->setAlignment(Qt::AlignLeft);
mainLayout->addWidget(m_statsLabel, 0);
m_leftDocument = QSharedPointer<TextEditor::TextDocument>::create();
m_rightDocument = QSharedPointer<TextEditor::TextDocument>::create();
Qt::Orientation initialOrientation = Settings::quickRefactorSettings().widgetOrientation.value() == 1
? Qt::Vertical : Qt::Horizontal;
m_splitter = new CustomSplitter(initialOrientation, this);
m_splitter->setChildrenCollapsible(false);
m_splitter->setHandleWidth(12);
m_splitter->setStyleSheet("QSplitter::handle { background-color: transparent; }");
m_leftEditor = new TextEditor::TextEditorWidget();
m_leftEditor->setTextDocument(m_leftDocument);
m_leftEditor->setReadOnly(true);
m_leftEditor->setFrameStyle(QFrame::StyledPanel);
m_leftEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_leftEditor->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_leftEditor->setMinimumWidth(150);
m_leftEditor->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_leftEditor->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
m_rightEditor = new TextEditor::TextEditorWidget();
m_rightEditor->setTextDocument(m_rightDocument);
m_rightEditor->setReadOnly(false);
m_rightEditor->setFrameStyle(QFrame::StyledPanel);
m_rightEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_rightEditor->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_rightEditor->setMinimumWidth(150);
m_rightEditor->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_leftContainer = new QWidget();
m_leftContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
auto *leftLayout = new QVBoxLayout(m_leftContainer);
leftLayout->setSpacing(2);
leftLayout->setContentsMargins(0, 0, 0, 0);
auto *originalLabel = new QLabel(tr("◄ Original"), m_leftContainer);
leftLayout->addWidget(originalLabel, 0);
leftLayout->addWidget(m_leftEditor, 1);
auto *rightContainer = new QWidget();
rightContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
auto *rightLayout = new QVBoxLayout(rightContainer);
rightLayout->setSpacing(2);
rightLayout->setContentsMargins(0, 0, 0, 0);
auto *refactoredLabel = new QLabel(tr("Refactored ►"), rightContainer);
rightLayout->addWidget(refactoredLabel, 0);
rightLayout->addWidget(m_rightEditor, 1);
m_splitter->addWidget(m_leftContainer);
m_splitter->addWidget(rightContainer);
m_splitter->setStretchFactor(0, 1);
m_splitter->setStretchFactor(1, 1);
mainLayout->addWidget(m_splitter, 1);
connect(m_leftEditor->verticalScrollBar(), &QScrollBar::valueChanged,
this, &RefactorWidget::syncLeftScroll);
connect(m_rightEditor->verticalScrollBar(), &QScrollBar::valueChanged,
this, &RefactorWidget::syncRightScroll);
connect(m_leftEditor->horizontalScrollBar(), &QScrollBar::valueChanged,
this, &RefactorWidget::syncLeftHorizontalScroll);
connect(m_rightEditor->horizontalScrollBar(), &QScrollBar::valueChanged,
this, &RefactorWidget::syncRightHorizontalScroll);
connect(m_rightDocument->document(), &QTextDocument::contentsChanged,
this, &RefactorWidget::onRightEditorTextChanged);
auto *buttonLayout = new QHBoxLayout();
buttonLayout->setContentsMargins(0, 2, 0, 0);
buttonLayout->setSpacing(6);
#ifdef Q_OS_MACOS
m_applyButton = new QPushButton(tr("✓ Apply (⌘+Enter)"), this);
#else
m_applyButton = new QPushButton(tr("✓ Apply (Ctrl+Enter)"), this);
#endif
m_applyButton->setFocusPolicy(Qt::NoFocus);
m_applyButton->setCursor(Qt::PointingHandCursor);
m_applyButton->setMaximumHeight(24);
m_declineButton = new QPushButton(tr("✗ Decline (Esc)"), this);
m_declineButton->setFocusPolicy(Qt::NoFocus);
m_declineButton->setCursor(Qt::PointingHandCursor);
m_declineButton->setMaximumHeight(24);
buttonLayout->addStretch();
buttonLayout->addWidget(m_applyButton);
buttonLayout->addWidget(m_declineButton);
mainLayout->addLayout(buttonLayout, 0);
connect(m_applyButton, &QPushButton::clicked, this, &RefactorWidget::applyRefactoring);
connect(m_declineButton, &QPushButton::clicked, this, &RefactorWidget::declineRefactoring);
}
void RefactorWidget::setDiffContent(const QString &originalText, const QString &refactoredText)
{
setDiffContent(originalText, refactoredText, QString(), QString());
}
void RefactorWidget::setDiffContent(const QString &originalText, const QString &refactoredText,
const QString &contextBefore, const QString &contextAfter)
{
m_originalText = originalText;
m_refactoredText = refactoredText;
m_contextBefore = contextBefore;
m_contextAfter = contextAfter;
m_leftContainer->setVisible(true);
QString leftFullText;
QString rightFullText;
if (!contextBefore.isEmpty()) {
leftFullText = contextBefore + "\n";
rightFullText = contextBefore + "\n";
}
leftFullText += originalText;
rightFullText += refactoredText;
if (!contextAfter.isEmpty()) {
leftFullText += "\n" + contextAfter;
rightFullText += "\n" + contextAfter;
}
m_leftDocument->setPlainText(leftFullText);
m_rightDocument->setPlainText(rightFullText);
applySyntaxHighlighting();
if (!contextBefore.isEmpty() || !contextAfter.isEmpty()) {
dimContextLines(contextBefore, contextAfter);
}
Utils::Differ differ;
m_cachedDiffList = differ.diff(m_originalText, m_refactoredText);
highlightDifferences();
addLineMarkers();
calculateStats();
updateStatsLabel();
updateSizeToContent();
}
void RefactorWidget::highlightDifferences()
{
if (m_cachedDiffList.isEmpty()) {
return;
}
QList<Utils::Diff> leftDiffs;
QList<Utils::Diff> rightDiffs;
Utils::Differ::splitDiffList(m_cachedDiffList, &leftDiffs, &rightDiffs);
int contextBeforeOffset = m_contextBefore.isEmpty() ? 0 : (m_contextBefore.length() + 1);
QColor normalTextColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
QTextCursor leftCursor(m_leftDocument->document());
QTextCharFormat removedFormat;
QColor removedBg = Utils::creatorColor(Utils::Theme::TextColorError);
removedBg.setAlpha(30);
removedFormat.setBackground(removedBg);
removedFormat.setForeground(normalTextColor);
int leftPos = 0;
for (const auto &diff : leftDiffs) {
if (diff.command == Utils::Diff::Delete) {
leftCursor.setPosition(contextBeforeOffset + leftPos);
leftCursor.setPosition(contextBeforeOffset + leftPos + diff.text.length(), QTextCursor::KeepAnchor);
leftCursor.setCharFormat(removedFormat);
}
if (diff.command != Utils::Diff::Insert) {
leftPos += diff.text.length();
}
}
QTextCursor rightCursor(m_rightDocument->document());
QTextCharFormat addedFormat;
QColor addedBg = Utils::creatorColor(Utils::Theme::IconsRunColor);
addedBg.setAlpha(60);
addedFormat.setBackground(addedBg);
addedFormat.setForeground(normalTextColor);
int rightPos = 0;
for (const auto &diff : rightDiffs) {
if (diff.command == Utils::Diff::Insert) {
rightCursor.setPosition(contextBeforeOffset + rightPos);
rightCursor.setPosition(contextBeforeOffset + rightPos + diff.text.length(), QTextCursor::KeepAnchor);
rightCursor.setCharFormat(addedFormat);
}
if (diff.command != Utils::Diff::Delete) {
rightPos += diff.text.length();
}
}
}
void RefactorWidget::dimContextLines(const QString &contextBefore, const QString &contextAfter)
{
QTextCharFormat dimFormat;
dimFormat.setForeground(Utils::creatorColor(Utils::Theme::TextColorDisabled));
auto dimLines = [&](QTextDocument *doc, int lineCount, bool fromStart) {
QTextCursor cursor(doc);
if (!fromStart) {
cursor.movePosition(QTextCursor::End);
}
for (int i = 0; i < lineCount; ++i) {
cursor.movePosition(QTextCursor::StartOfBlock);
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
cursor.setCharFormat(dimFormat);
if (fromStart) {
if (!cursor.block().isValid() || !cursor.movePosition(QTextCursor::NextBlock)) {
break;
}
} else {
if (!cursor.movePosition(QTextCursor::PreviousBlock)) {
break;
}
}
}
};
if (!contextBefore.isEmpty()) {
int lines = contextBefore.count('\n') + (contextBefore.endsWith('\n') ? 0 : 1);
dimLines(m_leftDocument->document(), lines, true);
dimLines(m_rightDocument->document(), lines, true);
}
if (!contextAfter.isEmpty()) {
int lines = contextAfter.count('\n') + (contextAfter.endsWith('\n') ? 0 : 1);
dimLines(m_leftDocument->document(), lines, false);
dimLines(m_rightDocument->document(), lines, false);
}
}
QString RefactorWidget::getRefactoredText() const
{
return m_applyText;
}
void RefactorWidget::setRange(const Utils::Text::Range &range)
{
m_range = range;
}
void RefactorWidget::setEditorWidth(int width)
{
m_editorWidth = width;
updateSizeToContent();
}
void RefactorWidget::setApplyCallback(std::function<void(const QString &)> callback)
{
m_applyCallback = callback;
}
void RefactorWidget::setDeclineCallback(std::function<void()> callback)
{
m_declineCallback = callback;
}
void RefactorWidget::applyRefactoring()
{
if (m_isClosing) return;
m_isClosing = true;
if (m_applyCallback) {
m_applyCallback(m_applyText);
}
emit applied();
close();
}
void RefactorWidget::declineRefactoring()
{
if (m_isClosing) return;
m_isClosing = true;
if (m_declineCallback) {
m_declineCallback();
}
emit declined();
close();
}
void RefactorWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QColor bgColor = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
const QColor borderColor = Utils::creatorColor(Utils::Theme::SplitterColor);
painter.fillRect(rect(), bgColor);
painter.setPen(QPen(borderColor, 2));
painter.drawRoundedRect(rect().adjusted(2, 2, -2, -2), 6, 6);
}
bool RefactorWidget::event(QEvent *event)
{
if (event->type() == QEvent::ShortcutOverride) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (((keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) &&
keyEvent->modifiers() == Qt::ControlModifier) ||
keyEvent->key() == Qt::Key_Escape) {
event->accept();
return true;
}
}
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if ((keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) &&
keyEvent->modifiers() == Qt::ControlModifier) {
applyRefactoring();
return true;
}
if (keyEvent->key() == Qt::Key_Escape) {
declineRefactoring();
return true;
}
}
return QWidget::event(event);
}
void RefactorWidget::syncLeftScroll(int value)
{
if (m_syncingScroll) return;
m_syncingScroll = true;
m_rightEditor->verticalScrollBar()->setValue(value);
m_syncingScroll = false;
}
void RefactorWidget::syncRightScroll(int value)
{
if (m_syncingScroll) return;
m_syncingScroll = true;
m_leftEditor->verticalScrollBar()->setValue(value);
m_syncingScroll = false;
}
void RefactorWidget::syncLeftHorizontalScroll(int value)
{
if (m_syncingScroll) return;
m_syncingScroll = true;
m_rightEditor->horizontalScrollBar()->setValue(value);
m_syncingScroll = false;
}
void RefactorWidget::syncRightHorizontalScroll(int value)
{
if (m_syncingScroll) return;
m_syncingScroll = true;
m_leftEditor->horizontalScrollBar()->setValue(value);
m_syncingScroll = false;
}
void RefactorWidget::onRightEditorTextChanged()
{
QString fullText = m_rightDocument->plainText();
int startPos = m_contextBefore.isEmpty() ? 0 : m_contextBefore.length() + 1;
int endPos = m_contextAfter.isEmpty() ? fullText.length() : fullText.length() - m_contextAfter.length() - 1;
m_applyText = fullText.mid(startPos, endPos - startPos);
}
void RefactorWidget::closeEvent(QCloseEvent *event)
{
if (!m_isClosing) {
declineRefactoring();
}
event->accept();
}
void RefactorWidget::calculateStats()
{
DiffStatistics stats;
stats.calculate(m_cachedDiffList);
m_linesAdded = stats.linesAdded();
m_linesRemoved = stats.linesRemoved();
}
void RefactorWidget::updateStatsLabel()
{
DiffStatistics stats;
stats.calculate(m_cachedDiffList);
m_statsLabel->setText("📊 " + stats.formatSummary());
}
void RefactorWidget::applySyntaxHighlighting()
{
if (!m_sourceEditor) {
return;
}
auto *sourceDoc = m_sourceEditor->textDocument();
if (!sourceDoc || !sourceDoc->syntaxHighlighter()) {
return;
}
m_leftDocument->setMimeType(sourceDoc->mimeType());
m_rightDocument->setMimeType(sourceDoc->mimeType());
}
void RefactorWidget::addLineMarkers()
{
if (m_cachedDiffList.isEmpty()) {
return;
}
QList<Utils::Diff> leftDiffs;
QList<Utils::Diff> rightDiffs;
Utils::Differ::splitDiffList(m_cachedDiffList, &leftDiffs, &rightDiffs);
int contextBeforeOffset = m_contextBefore.isEmpty() ? 0 : (m_contextBefore.length() + 1);
QColor removedMarker = Utils::creatorColor(Utils::Theme::TextColorError);
QColor addedMarker = Utils::creatorColor(Utils::Theme::IconsRunColor);
QTextCursor leftCursor(m_leftDocument->document());
int leftPos = 0;
for (const auto &diff : leftDiffs) {
if (diff.command == Utils::Diff::Delete) {
leftCursor.setPosition(contextBeforeOffset + leftPos);
leftCursor.movePosition(QTextCursor::StartOfBlock);
leftCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
QTextBlockFormat blockFormat;
blockFormat.setBackground(QBrush(removedMarker.lighter(185)));
blockFormat.setLeftMargin(4);
blockFormat.setProperty(QTextFormat::FullWidthSelection, true);
leftCursor.setBlockFormat(blockFormat);
}
if (diff.command != Utils::Diff::Insert) {
leftPos += diff.text.length();
}
}
QTextCursor rightCursor(m_rightDocument->document());
int rightPos = 0;
for (const auto &diff : rightDiffs) {
if (diff.command == Utils::Diff::Insert) {
rightCursor.setPosition(contextBeforeOffset + rightPos);
rightCursor.movePosition(QTextCursor::StartOfBlock);
rightCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
QTextBlockFormat blockFormat;
blockFormat.setBackground(QBrush(addedMarker.lighter(195)));
blockFormat.setLeftMargin(4);
blockFormat.setProperty(QTextFormat::FullWidthSelection, true);
rightCursor.setBlockFormat(blockFormat);
}
if (diff.command != Utils::Diff::Delete) {
rightPos += diff.text.length();
}
}
}
void RefactorWidget::updateSizeToContent()
{
QFontMetrics fm(m_rightEditor->font());
int lineHeight = fm.height();
int leftMaxWidth = 0;
int leftLineCount = 0;
QTextBlock leftBlock = m_leftDocument->document()->begin();
while (leftBlock.isValid()) {
int lineWidth = fm.horizontalAdvance(leftBlock.text());
leftMaxWidth = qMax(leftMaxWidth, lineWidth);
leftLineCount++;
leftBlock = leftBlock.next();
}
int rightMaxWidth = 0;
int rightLineCount = 0;
QTextBlock rightBlock = m_rightDocument->document()->begin();
while (rightBlock.isValid()) {
int lineWidth = fm.horizontalAdvance(rightBlock.text());
rightMaxWidth = qMax(rightMaxWidth, lineWidth);
rightLineCount++;
rightBlock = rightBlock.next();
}
int maxLineWidth = qMax(leftMaxWidth, rightMaxWidth);
int maxLineCount = qMax(leftLineCount, rightLineCount);
const int editorPadding = 60;
const int scrollBarWidth = 20;
const int statsLabelHeight = 30;
const int buttonLayoutHeight = 35;
const int layoutMargins = 20;
const int labelHeight = 20;
int singleEditorWidth = maxLineWidth + editorPadding + scrollBarWidth;
int contentHeight = maxLineCount * lineHeight + statsLabelHeight + buttonLayoutHeight + layoutMargins + labelHeight;
bool horizontal = m_splitter->orientation() == Qt::Horizontal;
int contentWidth;
if (horizontal) {
contentWidth = singleEditorWidth * 2 + m_splitter->handleWidth() + 20;
} else {
contentWidth = singleEditorWidth + 20;
}
QScreen *screen = window()->screen();
if (!screen) {
screen = QGuiApplication::primaryScreen();
}
int screenWidth = screen ? screen->availableGeometry().width() : 1920;
int screenHeight = screen ? screen->availableGeometry().height() : 1080;
const int minWidth = Settings::quickRefactorSettings().widgetMinWidth();
const int maxWidth = qMin(Settings::quickRefactorSettings().widgetMaxWidth(),
qMin(m_editorWidth - 40, screenWidth - 100));
const int minHeight = Settings::quickRefactorSettings().widgetMinHeight();
const int maxHeight = qMin(Settings::quickRefactorSettings().widgetMaxHeight(), screenHeight - 100);
int targetWidth = qBound(minWidth, contentWidth, maxWidth);
int targetHeight = qBound(minHeight, contentHeight, maxHeight);
setFixedSize(targetWidth, targetHeight);
updateGeometry();
}
void RefactorWidget::applyEditorSettings()
{
if (!m_sourceEditor || !m_leftEditor || !m_rightEditor) {
return;
}
QFont editorFont = m_sourceEditor->font();
m_leftEditor->setFont(editorFont);
m_rightEditor->setFont(editorFont);
QString labelStyle = QString("color: %1; padding: 2px 4px;")
.arg(Utils::creatorColor(Utils::Theme::TextColorDisabled).name());
for (auto *label : findChildren<QLabel *>()) {
if (label != m_statsLabel) {
QFont labelFont = label->font();
labelFont.setPointSize(qMax(8, editorFont.pointSize() - 2));
label->setFont(labelFont);
label->setStyleSheet(labelStyle);
}
}
QFont statsFont = m_statsLabel->font();
statsFont.setBold(true);
statsFont.setPointSize(qMax(9, editorFont.pointSize() - 1));
m_statsLabel->setFont(statsFont);
m_statsLabel->setStyleSheet(QString(
"color: %1; padding: 4px 6px; background-color: %2; border-radius: 3px;")
.arg(Utils::creatorColor(Utils::Theme::TextColorNormal).name())
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorHover).name()));
updateButtonStyles();
}
void RefactorWidget::updateButtonStyles()
{
if (!m_applyButton || !m_declineButton) {
return;
}
int baseFontSize = m_sourceEditor ? qMax(9, m_sourceEditor->font().pointSize() - 2) : 10;
auto createStyle = [&](const QColor &color, bool bold) {
return QString(
"QPushButton {"
" background-color: %1; color: %2; border: 1px solid %3;"
" border-radius: 3px; padding: 2px 8px; font-size: %4pt;%5"
"}"
"QPushButton:hover { background-color: %6; border: 1px solid %2; }"
"QPushButton:pressed { background-color: %7; }")
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorNormal).name())
.arg(color.name())
.arg(Utils::creatorColor(Utils::Theme::SplitterColor).name())
.arg(baseFontSize)
.arg(bold ? QLatin1StringView(" font-weight: bold;") : QLatin1StringView(""))
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorHover).name())
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorSelected).name());
};
m_applyButton->setStyleSheet(createStyle(Utils::creatorColor(Utils::Theme::TextColorNormal), true));
m_declineButton->setStyleSheet(createStyle(Utils::creatorColor(Utils::Theme::TextColorError), false));
}
} // namespace QodeAssist

View File

@@ -0,0 +1,126 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QKeyEvent>
#include <QEnterEvent>
#include <QSharedPointer>
#include <QSplitter>
#include <QSplitterHandle>
#include <functional>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <utils/differ.h>
namespace QodeAssist {
class CustomSplitterHandle : public QSplitterHandle
{
Q_OBJECT
public:
explicit CustomSplitterHandle(Qt::Orientation orientation, QSplitter *parent);
protected:
void paintEvent(QPaintEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
private:
bool m_hovered = false;
};
class CustomSplitter : public QSplitter
{
Q_OBJECT
public:
explicit CustomSplitter(Qt::Orientation orientation, QWidget *parent = nullptr);
protected:
QSplitterHandle *createHandle() override;
};
class RefactorWidget : public QWidget
{
Q_OBJECT
public:
explicit RefactorWidget(TextEditor::TextEditorWidget *sourceEditor, QWidget *parent = nullptr);
~RefactorWidget() override;
void setDiffContent(const QString &originalText, const QString &refactoredText);
void setDiffContent(const QString &originalText, const QString &refactoredText,
const QString &contextBefore, const QString &contextAfter);
void setApplyText(const QString &text) { m_applyText = text; }
void setRange(const Utils::Text::Range &range);
void setEditorWidth(int width);
QString getRefactoredText() const;
void setApplyCallback(std::function<void(const QString &)> callback);
void setDeclineCallback(std::function<void()> callback);
signals:
void applied();
void declined();
protected:
void paintEvent(QPaintEvent *event) override;
bool event(QEvent *event) override;
void closeEvent(QCloseEvent *event) override;
private slots:
void syncLeftScroll(int value);
void syncRightScroll(int value);
void syncLeftHorizontalScroll(int value);
void syncRightHorizontalScroll(int value);
void onRightEditorTextChanged();
private:
TextEditor::TextEditorWidget *m_sourceEditor;
TextEditor::TextEditorWidget *m_leftEditor;
TextEditor::TextEditorWidget *m_rightEditor;
QSharedPointer<TextEditor::TextDocument> m_leftDocument;
QSharedPointer<TextEditor::TextDocument> m_rightDocument;
QWidget *m_leftContainer;
QSplitter *m_splitter;
QLabel *m_statsLabel;
QPushButton *m_applyButton;
QPushButton *m_declineButton;
QString m_originalText;
QString m_refactoredText;
QString m_applyText;
QString m_contextBefore;
QString m_contextAfter;
Utils::Text::Range m_range;
int m_editorWidth;
bool m_syncingScroll;
bool m_isClosing;
int m_linesAdded;
int m_linesRemoved;
QList<Utils::Diff> m_cachedDiffList;
std::function<void(const QString &)> m_applyCallback;
std::function<void()> m_declineCallback;
void setupUi();
void applyRefactoring();
void declineRefactoring();
void updateSizeToContent();
void highlightDifferences();
void dimContextLines(const QString &contextBefore, const QString &contextAfter);
void calculateStats();
void updateStatsLabel();
void applySyntaxHighlighting();
void addLineMarkers();
void applyEditorSettings();
void updateButtonStyles();
};
} // namespace QodeAssist

View File

@@ -0,0 +1,148 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "RefactorWidgetHandler.hpp"
#include "RefactorWidget.hpp"
#include "ContextExtractor.hpp"
#include <QScrollBar>
#include <QTextBlock>
namespace QodeAssist {
RefactorWidgetHandler::RefactorWidgetHandler(QObject *parent)
: QObject(parent)
{
}
RefactorWidgetHandler::~RefactorWidgetHandler()
{
hideRefactorWidget();
}
void RefactorWidgetHandler::showRefactorWidget(
TextEditor::TextEditorWidget *editor,
const QString &originalText,
const QString &refactoredText,
const Utils::Text::Range &range)
{
QString contextBefore = ContextExtractor::extractBefore(editor, range, 3);
QString contextAfter = ContextExtractor::extractAfter(editor, range, 3);
showRefactorWidget(editor, originalText, refactoredText, range, contextBefore, contextAfter);
}
void RefactorWidgetHandler::showRefactorWidget(
TextEditor::TextEditorWidget *editor,
const QString &originalText,
const QString &refactoredText,
const Utils::Text::Range &range,
const QString &contextBefore,
const QString &contextAfter)
{
if (!editor) {
return;
}
hideRefactorWidget();
m_editor = editor;
m_refactorWidget = new RefactorWidget(editor);
m_refactorWidget->setDiffContent(originalText, refactoredText, contextBefore, contextAfter);
m_refactorWidget->setApplyText(refactoredText);
m_refactorWidget->setRange(range);
m_refactorWidget->setEditorWidth(getEditorWidth());
if (m_applyCallback) {
m_refactorWidget->setApplyCallback(m_applyCallback);
}
if (m_declineCallback) {
m_refactorWidget->setDeclineCallback(m_declineCallback);
}
updateWidgetPosition();
m_refactorWidget->show();
m_refactorWidget->raise();
}
void RefactorWidgetHandler::hideRefactorWidget()
{
if (!m_refactorWidget.isNull()) {
m_refactorWidget->close();
m_refactorWidget = nullptr;
}
m_editor = nullptr;
}
void RefactorWidgetHandler::setApplyCallback(std::function<void(const QString &)> callback)
{
m_applyCallback = callback;
}
void RefactorWidgetHandler::setDeclineCallback(std::function<void()> callback)
{
m_declineCallback = callback;
}
void RefactorWidgetHandler::setTextToApply(const QString &text)
{
if (!m_refactorWidget.isNull()) {
m_refactorWidget->setApplyText(text);
}
}
void RefactorWidgetHandler::updateWidgetPosition()
{
if (m_refactorWidget.isNull() || m_editor.isNull()) {
return;
}
QPoint position = calculateWidgetPosition();
m_refactorWidget->move(position);
}
QPoint RefactorWidgetHandler::calculateWidgetPosition()
{
if (m_editor.isNull()) {
return QPoint(0, 0);
}
QTextCursor cursor = m_editor->textCursor();
QRect cursorRect = m_editor->cursorRect(cursor);
QPoint globalPos = m_editor->mapToGlobal(cursorRect.bottomLeft());
globalPos.setY(globalPos.y() + 10);
if (m_refactorWidget) {
QRect widgetRect(globalPos, m_refactorWidget->size());
QRect screenRect = m_editor->screen()->availableGeometry();
if (widgetRect.right() > screenRect.right()) {
globalPos.setX(screenRect.right() - m_refactorWidget->width() - 10);
}
if (widgetRect.bottom() > screenRect.bottom()) {
globalPos.setY(m_editor->mapToGlobal(cursorRect.topLeft()).y()
- m_refactorWidget->height() - 10);
}
if (globalPos.x() < screenRect.left()) {
globalPos.setX(screenRect.left() + 10);
}
if (globalPos.y() < screenRect.top()) {
globalPos.setY(screenRect.top() + 10);
}
}
return globalPos;
}
int RefactorWidgetHandler::getEditorWidth() const
{
return m_editor.isNull() ? 800 : m_editor->viewport()->width();
}
} // namespace QodeAssist

View File

@@ -0,0 +1,60 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QPointer>
#include <functional>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
namespace QodeAssist {
class RefactorWidget;
class RefactorWidgetHandler : public QObject
{
Q_OBJECT
public:
explicit RefactorWidgetHandler(QObject *parent = nullptr);
~RefactorWidgetHandler() override;
void showRefactorWidget(
TextEditor::TextEditorWidget *editor,
const QString &originalText,
const QString &refactoredText,
const Utils::Text::Range &range);
void showRefactorWidget(
TextEditor::TextEditorWidget *editor,
const QString &originalText,
const QString &refactoredText,
const Utils::Text::Range &range,
const QString &contextBefore,
const QString &contextAfter);
void hideRefactorWidget();
bool isWidgetVisible() const { return !m_refactorWidget.isNull(); }
void setApplyCallback(std::function<void(const QString &)> callback);
void setDeclineCallback(std::function<void()> callback);
void setTextToApply(const QString &text);
private:
QPointer<TextEditor::TextEditorWidget> m_editor;
QPointer<RefactorWidget> m_refactorWidget;
std::function<void(const QString &)> m_applyCallback;
std::function<void()> m_declineCallback;
void updateWidgetPosition();
QPoint calculateWidgetPosition();
int getEditorWidth() const;
};
} // namespace QodeAssist