Compare commits

...

1 Commits

Author SHA1 Message Date
0aed886e31 feat: Add refactor editor widget 2025-11-23 13:14:44 +01:00
11 changed files with 1420 additions and 7 deletions

View File

@ -121,6 +121,8 @@ add_qtc_plugin(QodeAssist
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
widgets/RefactorWidget.hpp widgets/RefactorWidget.cpp
widgets/RefactorWidgetHandler.hpp widgets/RefactorWidgetHandler.cpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsFactory.hpp tools/ToolsFactory.cpp

View File

@ -40,6 +40,8 @@
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <context/ChangesManager.h>
#include <logger/Logger.hpp>
@ -71,12 +73,14 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
}
QodeAssistClient::~QodeAssistClient()
{
cleanupConnections();
delete m_refactorHoverHandler;
delete m_refactorWidgetHandler;
}
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
@ -451,11 +455,25 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
return;
}
TextEditorWidget *editorWidget = result.editor;
int displayMode = Settings::quickRefactorSettings().displayMode();
if (displayMode == 0) {
displayRefactoringWidget(result);
} else {
displayRefactoringSuggestion(result);
}
}
auto toTextPos = [](const Utils::Text::Position &pos) {
return Utils::Text::Position{pos.line, pos.column};
};
namespace {
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
{
return Utils::Text::Position{pos.line, pos.column};
}
} // anonymous namespace
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
@ -510,6 +528,68 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
}
void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
QString originalText;
const int startPos = range.begin.toPositionInDocument(editorWidget->document());
const int endPos = range.end.toPositionInDocument(editorWidget->document());
if (startPos != endPos) {
QTextCursor cursor(editorWidget->document());
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
originalText = cursor.selectedText();
originalText.replace(QChar(0x2029), "\n");
}
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
const Utils::Text::Range range{
Utils::Text::Position{result.insertRange.begin.line, result.insertRange.begin.column},
Utils::Text::Position{result.insertRange.end.line, result.insertRange.end.column}};
const QTextCursor startCursor = range.begin.toTextCursor(editorWidget->document());
const QTextCursor endCursor = range.end.toTextCursor(editorWidget->document());
const int startPos = startCursor.position();
const int endPos = endCursor.position();
QTextCursor editCursor(editorWidget->document());
editCursor.beginEditBlock();
if (startPos == endPos) {
editCursor.setPosition(startPos);
editCursor.insertText(editedText);
} else {
editCursor.setPosition(startPos);
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
editCursor.insertText(editedText);
}
editCursor.endEditBlock();
LOG_MESSAGE("Refactoring applied via widget with edited text");
});
m_refactorWidgetHandler->setDeclineCallback([this]() {
LOG_MESSAGE("Refactoring declined via widget");
});
m_refactorWidgetHandler->showRefactorWidget(
editorWidget,
originalText,
result.newText,
range);
LOG_MESSAGE(QString("Displaying refactoring widget - Original: %1 chars, New: %2 chars")
.arg(originalText.length())
.arg(result.newText.length()));
}
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
int charsAdded,
bool isSpaceOrTab)

View File

@ -34,6 +34,7 @@
#include "widgets/CompletionErrorHandler.hpp"
#include "widgets/CompletionHintHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
@ -70,6 +71,8 @@ private:
void setupConnections();
void cleanupConnections();
void handleRefactoringResult(const RefactorResult &result);
void displayRefactoringSuggestion(const RefactorResult &result);
void displayRefactoringWidget(const RefactorResult &result);
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
@ -88,6 +91,7 @@ private:
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient;
};

View File

@ -155,6 +155,16 @@ QuickRefactorSettings::QuickRefactorSettings()
readStringsAfterCursor.setRange(0, 10000);
readStringsAfterCursor.setDefaultValue(30);
displayMode.setSettingsKey(Constants::QR_DISPLAY_MODE);
displayMode.setLabelText(Tr::tr("Display Mode:"));
displayMode.setToolTip(
Tr::tr("Choose how to display refactoring suggestions:\n"
"- Inline Widget: Shows refactor in a widget overlay with Apply/Decline buttons (default)\n"
"- Qt Creator Suggestion: Uses Qt Creator's built-in suggestion system"));
displayMode.addOption(Tr::tr("Inline Widget"));
displayMode.addOption(Tr::tr("Qt Creator Suggestion"));
displayMode.setDefaultValue(0);
systemPrompt.setSettingsKey(Constants::QR_SYSTEM_PROMPT);
systemPrompt.setLabelText(Tr::tr("System Prompt:"));
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
@ -198,6 +208,9 @@ QuickRefactorSettings::QuickRefactorSettings()
contextGrid.addRow({Row{readFullFile}});
contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}});
auto displayGrid = Grid{};
displayGrid.addRow({Row{displayMode}});
return Column{
Row{Stretch{1}, resetToDefaults},
Space{8},
@ -212,6 +225,8 @@ QuickRefactorSettings::QuickRefactorSettings()
Space{8},
Group{title(Tr::tr("Context Settings")), Column{Row{contextGrid, Stretch{1}}}},
Space{8},
Group{title(Tr::tr("Display Settings")), Column{Row{displayGrid, Stretch{1}}}},
Space{8},
Group{title(Tr::tr("Prompt Settings")), Column{Row{systemPrompt}}},
Space{8},
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
@ -272,6 +287,7 @@ void QuickRefactorSettings::resetSettingsToDefaults()
resetAspect(readFileParts);
resetAspect(readStringsBeforeCursor);
resetAspect(readStringsAfterCursor);
resetAspect(displayMode);
resetAspect(systemPrompt);
writeSettings();
}

View File

@ -67,6 +67,9 @@ public:
Utils::IntegerAspect readStringsBeforeCursor{this};
Utils::IntegerAspect readStringsAfterCursor{this};
// Display Settings
Utils::SelectionAspect displayMode{this};
// Prompt Settings
Utils::StringAspect systemPrompt{this};

View File

@ -220,5 +220,6 @@ const char QR_READ_FULL_FILE[] = "QodeAssist.qrReadFullFile";
const char QR_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.qrReadStringsBeforeCursor";
const char QR_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.qrReadStringsAfterCursor";
const char QR_SYSTEM_PROMPT[] = "QodeAssist.qrSystemPrompt";
const char QR_DISPLAY_MODE[] = "QodeAssist.qrDisplayMode";
} // namespace QodeAssist::Constants

View File

@ -226,9 +226,12 @@ QStringList ExecuteTerminalCommandTool::getAllowedCommands() const
return QStringList();
}
QStringList commands = commandsStr.split(',', Qt::SkipEmptyParts);
for (QString &cmd : commands) {
cmd = cmd.trimmed();
const QStringList rawCommands = commandsStr.split(',', Qt::SkipEmptyParts);
QStringList commands;
commands.reserve(rawCommands.size());
for (const QString &cmd : rawCommands) {
commands.append(cmd.trimmed());
}
return commands;

816
widgets/RefactorWidget.cpp Normal file
View File

@ -0,0 +1,816 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "RefactorWidget.hpp"
#include <texteditor/textdocument.h>
#include <texteditor/syntaxhighlighter.h>
#include <QCloseEvent>
#include <QEnterEvent>
#include <QEvent>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QPainter>
#include <QRegion>
#include <QScrollBar>
#include <QSharedPointer>
#include <QSplitter>
#include <QTextBlock>
#include <QVBoxLayout>
#include <utils/differ.h>
#include <utils/plaintextedit/plaintextedit.h>
#include <utils/theme/theme.h>
namespace QodeAssist {
namespace {
QString createButtonStyleSheet(const QColor &normalBg, const QColor &textColor,
const QColor &borderColor, const QColor &hoverBg,
const QColor &selectedBg, bool boldText = false,
const QColor &checkedBg = QColor(), const QColor &checkedTextColor = QColor())
{
QString style = QString(
"QPushButton {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" border-radius: 3px;"
" padding: 3px 10px;"
" font-size: 11px;"
"%4"
"}"
"QPushButton:hover {"
" background-color: %5;"
" border: 1px solid %2;"
"}"
"QPushButton:pressed {"
" background-color: %6;"
"}")
.arg(normalBg.name(),
textColor.name(),
borderColor.name(),
boldText ? " font-weight: bold;" : "",
hoverBg.name(),
selectedBg.name());
if (checkedBg.isValid() && checkedTextColor.isValid()) {
style += QString(
"\nQPushButton:checked {"
" background-color: %1;"
" color: %2;"
" font-weight: bold;"
"}")
.arg(checkedBg.name(), checkedTextColor.name());
}
return style;
}
} // anonymous namespace
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 *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QColor bgColor = Utils::creatorColor(Utils::Theme::BackgroundColorHover);
if (m_hovered) {
bgColor.setAlpha(150);
} else {
bgColor.setAlpha(50);
}
painter.fillRect(rect(), bgColor);
QColor lineColor = Utils::creatorColor(Utils::Theme::SplitterColor);
lineColor.setAlpha(m_hovered ? 255 : 180);
painter.setPen(QPen(lineColor, m_hovered ? 3 : 2));
if (orientation() == Qt::Horizontal) {
int x = width() / 2;
painter.drawLine(x, 10, x, height() - 10);
painter.setBrush(lineColor);
int centerY = height() / 2;
int dotSize = m_hovered ? 3 : 2;
for (int i = -2; i <= 2; ++i) {
painter.drawEllipse(QPoint(x, centerY + i * 8), dotSize, dotSize);
}
} else {
int y = height() / 2;
painter.drawLine(10, y, width() - 10, y);
}
}
void CustomSplitterHandle::enterEvent(QEnterEvent *event)
{
Q_UNUSED(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_toggleOriginalButton(nullptr)
, m_applyButton(nullptr)
, m_declineButton(nullptr)
, m_editorWidth(800)
, m_syncingScroll(false)
, m_linesAdded(0)
, m_linesRemoved(0)
{
setupUi();
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
setAttribute(Qt::WA_DeleteOnClose);
setFocusPolicy(Qt::StrongFocus);
}
RefactorWidget::~RefactorWidget()
{
}
void RefactorWidget::setupUi()
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(8, 8, 8, 8);
mainLayout->setSpacing(4);
m_statsLabel = new QLabel(this);
m_statsLabel->setStyleSheet(QString(
"color: %1; font-size: 11px; font-weight: bold; padding: 4px 6px; "
"background-color: %2; border-radius: 3px;")
.arg(Utils::creatorColor(Utils::Theme::TextColorNormal).name())
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorHover).name()));
m_statsLabel->setAlignment(Qt::AlignLeft);
mainLayout->addWidget(m_statsLabel);
m_leftDocument = QSharedPointer<TextEditor::TextDocument>::create();
m_rightDocument = QSharedPointer<TextEditor::TextDocument>::create();
m_splitter = new CustomSplitter(Qt::Horizontal, this);
m_splitter->setChildrenCollapsible(false);
m_splitter->setHandleWidth(16);
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->setLineWrapMode(Utils::PlainTextEdit::NoWrap);
m_leftEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_leftEditor->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_leftEditor->setMinimumHeight(120);
m_leftEditor->setMinimumWidth(200);
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->setLineWrapMode(Utils::PlainTextEdit::NoWrap);
m_rightEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_rightEditor->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_rightEditor->setMinimumHeight(120);
m_rightEditor->setMinimumWidth(200);
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);
originalLabel->setStyleSheet("color: " + Utils::creatorColor(Utils::Theme::TextColorDisabled).name() +
"; font-size: 10px; padding: 2px 4px;");
leftLayout->addWidget(originalLabel);
leftLayout->addWidget(m_leftEditor);
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);
refactoredLabel->setStyleSheet("color: " + Utils::creatorColor(Utils::Theme::TextColorDisabled).name() +
"; font-size: 10px; padding: 2px 4px;");
rightLayout->addWidget(refactoredLabel);
rightLayout->addWidget(m_rightEditor);
m_splitter->addWidget(m_leftContainer);
m_splitter->addWidget(rightContainer);
m_splitter->setStretchFactor(0, 1);
m_splitter->setStretchFactor(1, 1);
mainLayout->addWidget(m_splitter);
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);
const QColor normalBg = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
const QColor borderColor = Utils::creatorColor(Utils::Theme::SplitterColor);
const QColor hoverBg = Utils::creatorColor(Utils::Theme::BackgroundColorHover);
const QColor selectedBg = Utils::creatorColor(Utils::Theme::BackgroundColorSelected);
const QColor successColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
const QColor errorColor = Utils::creatorColor(Utils::Theme::TextColorError);
const QColor infoColor = Utils::creatorColor(Utils::Theme::TextColorDisabled);
m_toggleOriginalButton = new QPushButton(tr("◄ Show Original"), this);
m_toggleOriginalButton->setFocusPolicy(Qt::NoFocus);
m_toggleOriginalButton->setCursor(Qt::PointingHandCursor);
m_toggleOriginalButton->setMaximumHeight(26);
m_toggleOriginalButton->setCheckable(true);
m_toggleOriginalButton->setChecked(false);
#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(26);
m_declineButton = new QPushButton(tr("✗ Decline (Esc)"), this);
m_declineButton->setFocusPolicy(Qt::NoFocus);
m_declineButton->setCursor(Qt::PointingHandCursor);
m_declineButton->setMaximumHeight(26);
m_applyButton->setStyleSheet(createButtonStyleSheet(normalBg, successColor, borderColor,
hoverBg, selectedBg, true));
m_declineButton->setStyleSheet(createButtonStyleSheet(normalBg, errorColor, borderColor,
hoverBg, selectedBg, false));
m_toggleOriginalButton->setStyleSheet(createButtonStyleSheet(normalBg, infoColor, borderColor,
hoverBg, selectedBg, false,
selectedBg, successColor));
buttonLayout->addWidget(m_toggleOriginalButton);
buttonLayout->addStretch();
buttonLayout->addWidget(m_applyButton);
buttonLayout->addWidget(m_declineButton);
mainLayout->addLayout(buttonLayout);
connect(m_applyButton, &QPushButton::clicked, this, &RefactorWidget::applyRefactoring);
connect(m_declineButton, &QPushButton::clicked, this, &RefactorWidget::declineRefactoring);
connect(m_toggleOriginalButton, &QPushButton::toggled, this, &RefactorWidget::toggleOriginalVisibility);
}
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(false);
bool hasOriginal = !originalText.trimmed().isEmpty();
m_toggleOriginalButton->setEnabled(hasOriginal);
m_toggleOriginalButton->setChecked(false);
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;
QColor dimColor = Utils::creatorColor(Utils::Theme::TextColorDisabled);
dimFormat.setForeground(dimColor);
if (!contextBefore.isEmpty()) {
int contextBeforeLines = contextBefore.count('\n');
if (!contextBefore.endsWith('\n')) {
contextBeforeLines++;
}
QTextCursor leftCursor(m_leftDocument->document());
for (int i = 0; i < contextBeforeLines && leftCursor.block().isValid(); ++i) {
leftCursor.movePosition(QTextCursor::StartOfBlock);
leftCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
leftCursor.setCharFormat(dimFormat);
leftCursor.movePosition(QTextCursor::NextBlock);
}
QTextCursor rightCursor(m_rightDocument->document());
for (int i = 0; i < contextBeforeLines && rightCursor.block().isValid(); ++i) {
rightCursor.movePosition(QTextCursor::StartOfBlock);
rightCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
rightCursor.setCharFormat(dimFormat);
rightCursor.movePosition(QTextCursor::NextBlock);
}
}
if (!contextAfter.isEmpty()) {
int contextAfterLines = contextAfter.count('\n');
if (!contextAfter.endsWith('\n')) {
contextAfterLines++;
}
QTextCursor leftCursor(m_leftDocument->document());
leftCursor.movePosition(QTextCursor::End);
for (int i = 0; i < contextAfterLines; ++i) {
leftCursor.movePosition(QTextCursor::StartOfBlock);
leftCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
leftCursor.setCharFormat(dimFormat);
if (!leftCursor.movePosition(QTextCursor::PreviousBlock)) {
break;
}
}
QTextCursor rightCursor(m_rightDocument->document());
rightCursor.movePosition(QTextCursor::End);
for (int i = 0; i < contextAfterLines; ++i) {
rightCursor.movePosition(QTextCursor::StartOfBlock);
rightCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
rightCursor.setCharFormat(dimFormat);
if (!rightCursor.movePosition(QTextCursor::PreviousBlock)) {
break;
}
}
}
}
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_applyCallback) {
m_applyCallback(m_applyText);
}
emit applied();
close();
}
void RefactorWidget::declineRefactoring()
{
if (m_declineCallback) {
m_declineCallback();
}
emit declined();
close();
}
void RefactorWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
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 = 0;
int endPos = fullText.length();
if (!m_contextBefore.isEmpty()) {
startPos = m_contextBefore.length() + 1;
}
if (!m_contextAfter.isEmpty()) {
endPos = fullText.length() - m_contextAfter.length() - 1;
}
QString editedText = fullText.mid(startPos, endPos - startPos);
if (!m_displayIndent.isEmpty()) {
if (editedText.startsWith(m_displayIndent)) {
editedText = editedText.mid(m_displayIndent.length());
}
}
m_applyText = editedText;
}
void RefactorWidget::toggleOriginalVisibility()
{
bool isVisible = m_toggleOriginalButton->isChecked();
m_leftContainer->setVisible(isVisible);
if (isVisible) {
m_toggleOriginalButton->setText(tr("► Hide Original"));
} else {
m_toggleOriginalButton->setText(tr("◄ Show Original"));
}
updateSizeToContent();
}
void RefactorWidget::closeEvent(QCloseEvent *event)
{
declineRefactoring();
event->accept();
}
void RefactorWidget::calculateStats()
{
m_linesAdded = 0;
m_linesRemoved = 0;
for (const auto &diff : m_cachedDiffList) {
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);
}
}
}
void RefactorWidget::updateStatsLabel()
{
QString statsText;
if (m_linesAdded > 0 && m_linesRemoved > 0) {
statsText = tr("+%1 lines, -%2 lines").arg(m_linesAdded).arg(m_linesRemoved);
} else if (m_linesAdded > 0) {
statsText = tr("+%1 lines").arg(m_linesAdded);
} else if (m_linesRemoved > 0) {
statsText = tr("-%1 lines").arg(m_linesRemoved);
} else {
statsText = tr("No changes");
}
m_statsLabel->setText("📊 " + statsText);
}
void RefactorWidget::applySyntaxHighlighting()
{
if (!m_sourceEditor) {
return;
}
// Get the syntax highlighter from source editor
auto *sourceDoc = m_sourceEditor->textDocument();
if (!sourceDoc) {
return;
}
auto *sourceHighlighter = sourceDoc->syntaxHighlighter();
if (!sourceHighlighter) {
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 charWidth = fm.horizontalAdvance('m');
int singleEditorWidth = charWidth * 85 + 80;
int totalWidth;
if (m_leftContainer->isVisible()) {
totalWidth = singleEditorWidth * 2 + 30;
} else {
totalWidth = singleEditorWidth + 20;
}
int minWidth = 650;
int maxWidth = qMin(m_editorWidth - 40, 1600);
totalWidth = qBound(minWidth, totalWidth, maxWidth);
setFixedWidth(totalWidth);
int lineHeight = fm.height();
int lineCount = m_rightDocument->document()->blockCount();
int contentHeight = lineCount * lineHeight;
int minHeight = 150;
int maxHeight = 600;
int totalHeight = 90 + qBound(minHeight, contentHeight, maxHeight);
int editorHeight = qBound(minHeight, contentHeight, maxHeight);
m_leftEditor->setMinimumHeight(editorHeight);
m_leftEditor->setMaximumHeight(editorHeight);
m_rightEditor->setMinimumHeight(editorHeight);
m_rightEditor->setMaximumHeight(editorHeight);
updateGeometry();
adjustSize();
}
} // namespace QodeAssist

142
widgets/RefactorWidget.hpp Normal file
View File

@ -0,0 +1,142 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <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 setDisplayIndent(const QString &indent) { m_displayIndent = indent; }
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();
void toggleOriginalVisibility();
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_toggleOriginalButton;
QPushButton *m_applyButton;
QPushButton *m_declineButton;
QString m_originalText;
QString m_refactoredText;
QString m_applyText;
QString m_contextBefore;
QString m_contextAfter;
QString m_displayIndent;
Utils::Text::Range m_range;
int m_editorWidth;
bool m_syncingScroll;
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();
};
} // namespace QodeAssist

View File

@ -0,0 +1,271 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "RefactorWidgetHandler.hpp"
#include "RefactorWidget.hpp"
#include <QScrollBar>
#include <QTextBlock>
#include <logger/Logger.hpp>
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)
{
if (!editor) {
LOG_MESSAGE("RefactorWidgetHandler: No editor widget");
return;
}
hideRefactorWidget();
m_editor = editor;
m_refactorWidget = new RefactorWidget(editor);
QString contextBefore = extractContextBefore(editor, range, 3);
QString contextAfter = extractContextAfter(editor, range, 3);
QString baseIndentation = extractIndentation(editor, range);
QString displayOriginalText = originalText;
QString displayRefactoredText = refactoredText;
if (!baseIndentation.isEmpty()) {
displayOriginalText = applyIndentationToFirstLine(originalText, baseIndentation);
displayRefactoredText = applyIndentationToFirstLine(refactoredText, baseIndentation);
}
m_refactorWidget->setDiffContent(displayOriginalText, displayRefactoredText,
contextBefore, contextAfter);
m_refactorWidget->setApplyText(refactoredText);
m_refactorWidget->setDisplayIndent(baseIndentation);
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();
LOG_MESSAGE("RefactorWidgetHandler: Showing unified diff widget (read-only)");
}
void RefactorWidgetHandler::hideRefactorWidget()
{
if (!m_refactorWidget.isNull()) {
if (m_editor) {
m_editor->removeEventFilter(m_refactorWidget);
}
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::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);
}
// Get cursor position
QTextCursor cursor = m_editor->textCursor();
QRect cursorRect = m_editor->cursorRect(cursor);
// Convert to global coordinates
QPoint globalPos = m_editor->mapToGlobal(cursorRect.bottomLeft());
// Adjust position to avoid overlapping with cursor
globalPos.setY(globalPos.y() + 10);
// Ensure widget stays within screen bounds
if (m_refactorWidget) {
QRect widgetRect(globalPos, m_refactorWidget->size());
QRect screenRect = m_editor->screen()->availableGeometry();
// Move left if widget goes off right edge
if (widgetRect.right() > screenRect.right()) {
globalPos.setX(screenRect.right() - m_refactorWidget->width() - 10);
}
// Move up if widget goes off bottom edge
if (widgetRect.bottom() > screenRect.bottom()) {
globalPos.setY(m_editor->mapToGlobal(cursorRect.topLeft()).y()
- m_refactorWidget->height() - 10);
}
// Ensure not too far left
if (globalPos.x() < screenRect.left()) {
globalPos.setX(screenRect.left() + 10);
}
// Ensure not too far up
if (globalPos.y() < screenRect.top()) {
globalPos.setY(screenRect.top() + 10);
}
}
return globalPos;
}
int RefactorWidgetHandler::getEditorWidth() const
{
if (m_editor.isNull()) {
return 800;
}
return m_editor->viewport()->width();
}
QString RefactorWidgetHandler::extractIndentation(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range) const
{
if (!editor) {
return QString();
}
QTextDocument *doc = editor->document();
QTextBlock block = doc->findBlockByNumber(range.begin.line - 1);
if (!block.isValid()) {
return QString();
}
QString lineText = block.text();
QString indentation;
for (const QChar &ch : lineText) {
if (ch.isSpace()) {
indentation += ch;
} else {
break;
}
}
return indentation;
}
QString RefactorWidgetHandler::applyIndentationToFirstLine(const QString &text, const QString &indentation) const
{
if (indentation.isEmpty() || text.isEmpty()) {
return text;
}
QStringList lines = text.split('\n');
if (lines.isEmpty()) {
return text;
}
lines[0] = indentation + lines[0];
return lines.join('\n');
}
QString RefactorWidgetHandler::extractContextBefore(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
int lineCount) const
{
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');
}
QString RefactorWidgetHandler::extractContextAfter(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
int lineCount) const
{
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');
}
} // namespace QodeAssist

View File

@ -0,0 +1,75 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <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 hideRefactorWidget();
bool isWidgetVisible() const { return !m_refactorWidget.isNull(); }
void setApplyCallback(std::function<void(const QString &)> callback);
void setDeclineCallback(std::function<void()> callback);
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;
QString extractIndentation(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range) const;
QString applyIndentationToFirstLine(const QString &text, const QString &indentation) const;
QString extractContextBefore(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
int lineCount) const;
QString extractContextAfter(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
int lineCount) const;
};
} // namespace QodeAssist