diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c8e11b..a85bb64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index aadcfe2..ef0ae35 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -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 #include @@ -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) diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index 2a127f5..145cb10 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -34,6 +34,7 @@ #include "widgets/CompletionErrorHandler.hpp" #include "widgets/CompletionHintHandler.hpp" #include "widgets/EditorChatButtonHandler.hpp" +#include "widgets/RefactorWidgetHandler.hpp" #include #include #include @@ -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; }; diff --git a/settings/QuickRefactorSettings.cpp b/settings/QuickRefactorSettings.cpp index a188b2a..e59f576 100644 --- a/settings/QuickRefactorSettings.cpp +++ b/settings/QuickRefactorSettings.cpp @@ -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(); } diff --git a/settings/QuickRefactorSettings.hpp b/settings/QuickRefactorSettings.hpp index 05f3bf4..fa9bf91 100644 --- a/settings/QuickRefactorSettings.hpp +++ b/settings/QuickRefactorSettings.hpp @@ -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}; diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 3ce74f1..b4e6083 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -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 diff --git a/tools/ExecuteTerminalCommandTool.cpp b/tools/ExecuteTerminalCommandTool.cpp index 4440bc2..8bf9215 100644 --- a/tools/ExecuteTerminalCommandTool.cpp +++ b/tools/ExecuteTerminalCommandTool.cpp @@ -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; diff --git a/widgets/RefactorWidget.cpp b/widgets/RefactorWidget.cpp new file mode 100644 index 0000000..d812d55 --- /dev/null +++ b/widgets/RefactorWidget.cpp @@ -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 . + */ + +#include "RefactorWidget.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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::create(); + m_rightDocument = QSharedPointer::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 leftDiffs; + QList 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 callback) +{ + m_applyCallback = callback; +} + +void RefactorWidget::setDeclineCallback(std::function 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(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(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 leftDiffs; + QList 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 diff --git a/widgets/RefactorWidget.hpp b/widgets/RefactorWidget.hpp new file mode 100644 index 0000000..312d1e2 --- /dev/null +++ b/widgets/RefactorWidget.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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 callback); + void setDeclineCallback(std::function 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 m_leftDocument; + QSharedPointer 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 m_cachedDiffList; + + std::function m_applyCallback; + std::function 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 diff --git a/widgets/RefactorWidgetHandler.cpp b/widgets/RefactorWidgetHandler.cpp new file mode 100644 index 0000000..ac297dd --- /dev/null +++ b/widgets/RefactorWidgetHandler.cpp @@ -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 . + */ + +#include "RefactorWidgetHandler.hpp" +#include "RefactorWidget.hpp" + +#include +#include + +#include + +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 callback) +{ + m_applyCallback = callback; +} + +void RefactorWidgetHandler::setDeclineCallback(std::function 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 + diff --git a/widgets/RefactorWidgetHandler.hpp b/widgets/RefactorWidgetHandler.hpp new file mode 100644 index 0000000..7d964e3 --- /dev/null +++ b/widgets/RefactorWidgetHandler.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +#include +#include + +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 callback); + void setDeclineCallback(std::function callback); + +private: + QPointer m_editor; + QPointer m_refactorWidget; + + std::function m_applyCallback; + std::function 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 +