mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-04-12 07:42:52 -04:00
feat: Add side by side refactor widget (#276)
This commit is contained in:
@ -69,6 +69,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssistConstants.hpp
|
QodeAssistConstants.hpp
|
||||||
QodeAssisttr.h
|
QodeAssisttr.h
|
||||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||||
|
RefactorContextHelper.hpp
|
||||||
templates/Templates.hpp
|
templates/Templates.hpp
|
||||||
templates/CodeLlamaFim.hpp
|
templates/CodeLlamaFim.hpp
|
||||||
templates/Ollama.hpp
|
templates/Ollama.hpp
|
||||||
@ -121,6 +122,10 @@ add_qtc_plugin(QodeAssist
|
|||||||
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
||||||
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
||||||
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
||||||
|
widgets/RefactorWidget.hpp widgets/RefactorWidget.cpp
|
||||||
|
widgets/RefactorWidgetHandler.hpp widgets/RefactorWidgetHandler.cpp
|
||||||
|
widgets/ContextExtractor.hpp
|
||||||
|
widgets/DiffStatistics.hpp
|
||||||
|
|
||||||
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
||||||
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
|
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
|
||||||
|
|||||||
@ -40,6 +40,9 @@
|
|||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include "settings/ProjectSettings.hpp"
|
#include "settings/ProjectSettings.hpp"
|
||||||
|
#include "settings/QuickRefactorSettings.hpp"
|
||||||
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
|
#include "RefactorContextHelper.hpp"
|
||||||
#include <context/ChangesManager.h>
|
#include <context/ChangesManager.h>
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
|
|
||||||
@ -71,12 +74,14 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
|
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
|
||||||
|
|
||||||
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
||||||
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
QodeAssistClient::~QodeAssistClient()
|
QodeAssistClient::~QodeAssistClient()
|
||||||
{
|
{
|
||||||
cleanupConnections();
|
cleanupConnections();
|
||||||
delete m_refactorHoverHandler;
|
delete m_refactorHoverHandler;
|
||||||
|
delete m_refactorWidgetHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||||
@ -451,11 +456,25 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditorWidget *editorWidget = result.editor;
|
int displayMode = Settings::quickRefactorSettings().displayMode();
|
||||||
|
|
||||||
auto toTextPos = [](const Utils::Text::Position &pos) {
|
if (displayMode == 0) {
|
||||||
return Utils::Text::Position{pos.line, pos.column};
|
displayRefactoringWidget(result);
|
||||||
};
|
} else {
|
||||||
|
displayRefactoringSuggestion(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
||||||
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
|
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
|
||||||
@ -510,6 +529,81 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
|
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)};
|
||||||
|
|
||||||
|
RefactorContext ctx = RefactorContextHelper::extractContext(editorWidget, range);
|
||||||
|
|
||||||
|
QString displayOriginal;
|
||||||
|
QString displayRefactored;
|
||||||
|
QString textToApply = result.newText;
|
||||||
|
|
||||||
|
if (ctx.isInsertion) {
|
||||||
|
bool isMultiline = result.newText.contains('\n');
|
||||||
|
|
||||||
|
if (isMultiline) {
|
||||||
|
displayOriginal = ctx.textBeforeCursor;
|
||||||
|
displayRefactored = ctx.textBeforeCursor + result.newText;
|
||||||
|
} else {
|
||||||
|
displayOriginal = ctx.textBeforeCursor + ctx.textAfterCursor;
|
||||||
|
displayRefactored = ctx.textBeforeCursor + result.newText + ctx.textAfterCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.textBeforeCursor.isEmpty() || !ctx.textAfterCursor.isEmpty()) {
|
||||||
|
textToApply = result.newText;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayOriginal = ctx.originalText;
|
||||||
|
displayRefactored = result.newText;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
|
||||||
|
applyRefactoringEdit(editorWidget, result.insertRange, editedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setDeclineCallback([]() {});
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->showRefactorWidget(
|
||||||
|
editorWidget, displayOriginal, displayRefactored, range,
|
||||||
|
ctx.contextBefore, ctx.contextAfter);
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setTextToApply(textToApply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
const QString &text)
|
||||||
|
{
|
||||||
|
const QTextCursor startCursor = range.begin.toTextCursor(editor->document());
|
||||||
|
const QTextCursor endCursor = range.end.toTextCursor(editor->document());
|
||||||
|
const int startPos = startCursor.position();
|
||||||
|
const int endPos = endCursor.position();
|
||||||
|
|
||||||
|
QTextCursor editCursor(editor->document());
|
||||||
|
editCursor.beginEditBlock();
|
||||||
|
|
||||||
|
if (startPos == endPos) {
|
||||||
|
bool isMultiline = text.contains('\n');
|
||||||
|
editCursor.setPosition(startPos);
|
||||||
|
|
||||||
|
if (isMultiline) {
|
||||||
|
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.insertText(text);
|
||||||
|
} else {
|
||||||
|
editCursor.setPosition(startPos);
|
||||||
|
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
editCursor.insertText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.endEditBlock();
|
||||||
|
}
|
||||||
|
|
||||||
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
|
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
|
||||||
int charsAdded,
|
int charsAdded,
|
||||||
bool isSpaceOrTab)
|
bool isSpaceOrTab)
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
#include "widgets/CompletionErrorHandler.hpp"
|
#include "widgets/CompletionErrorHandler.hpp"
|
||||||
#include "widgets/CompletionHintHandler.hpp"
|
#include "widgets/CompletionHintHandler.hpp"
|
||||||
#include "widgets/EditorChatButtonHandler.hpp"
|
#include "widgets/EditorChatButtonHandler.hpp"
|
||||||
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
#include <languageclient/client.h>
|
#include <languageclient/client.h>
|
||||||
#include <llmcore/IPromptProvider.hpp>
|
#include <llmcore/IPromptProvider.hpp>
|
||||||
#include <llmcore/IProviderRegistry.hpp>
|
#include <llmcore/IProviderRegistry.hpp>
|
||||||
@ -70,6 +71,9 @@ private:
|
|||||||
void setupConnections();
|
void setupConnections();
|
||||||
void cleanupConnections();
|
void cleanupConnections();
|
||||||
void handleRefactoringResult(const RefactorResult &result);
|
void handleRefactoringResult(const RefactorResult &result);
|
||||||
|
void displayRefactoringSuggestion(const RefactorResult &result);
|
||||||
|
void displayRefactoringWidget(const RefactorResult &result);
|
||||||
|
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
|
||||||
|
|
||||||
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
|
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
|
||||||
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
|
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
|
||||||
@ -88,6 +92,7 @@ private:
|
|||||||
EditorChatButtonHandler m_chatButtonHandler;
|
EditorChatButtonHandler m_chatButtonHandler;
|
||||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||||
|
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
||||||
LLMClientInterface *m_llmClient;
|
LLMClientInterface *m_llmClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#include <context/DocumentContextReader.hpp>
|
#include <context/DocumentContextReader.hpp>
|
||||||
|
#include <llmcore/ResponseCleaner.hpp>
|
||||||
#include <context/DocumentReaderQtCreator.hpp>
|
#include <context/DocumentReaderQtCreator.hpp>
|
||||||
#include <context/Utils.hpp>
|
#include <context/Utils.hpp>
|
||||||
#include <llmcore/PromptTemplateManager.hpp>
|
#include <llmcore/PromptTemplateManager.hpp>
|
||||||
@ -301,17 +302,65 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
||||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
||||||
|
|
||||||
systemPrompt += "\n\nCode context with position markers:";
|
systemPrompt += "\n\n# Code Context with Position Markers\n" + taggedContent;
|
||||||
systemPrompt += taggedContent;
|
|
||||||
|
|
||||||
systemPrompt += "\n\nOutput format:";
|
systemPrompt += "\n\n# Output Requirements\n## What to Generate:";
|
||||||
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
|
systemPrompt += cursor.hasSelection()
|
||||||
"between<selection_start><selection_end> or be "
|
? "\n- Generate ONLY the code that should REPLACE the selected text between "
|
||||||
"inserted at cursor position<cursor>";
|
"<selection_start> and <selection_end> markers"
|
||||||
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
|
"\n- Your output will completely replace the selected code"
|
||||||
"code block markers";
|
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
||||||
systemPrompt += "\n- The output should be ready to insert directly into the editor";
|
"\n- Your output will be inserted at the cursor location";
|
||||||
systemPrompt += "\n- Follow the existing code style and indentation patterns";
|
|
||||||
|
systemPrompt += "\n\n## Formatting Rules:"
|
||||||
|
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
||||||
|
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
||||||
|
"\n- Do NOT add comments explaining what you changed"
|
||||||
|
"\n- Do NOT repeat existing code, be precise with context"
|
||||||
|
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
||||||
|
"\n- The output must be ready to insert directly into the editor as-is";
|
||||||
|
|
||||||
|
systemPrompt += "\n\n## Indentation and Whitespace:";
|
||||||
|
|
||||||
|
if (cursor.hasSelection()) {
|
||||||
|
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (QChar c : startBlock.text()) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else if (c == '\t') leadingSpaces += 4;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (leadingSpaces > 0) {
|
||||||
|
systemPrompt += QString("\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
|
||||||
|
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
|
||||||
|
"\n- Each line in your output must maintain this base indentation")
|
||||||
|
.arg(leadingSpaces);
|
||||||
|
}
|
||||||
|
systemPrompt += "\n- PRESERVE all indentation from the original code";
|
||||||
|
} else {
|
||||||
|
QTextBlock block = documentInfo.document->findBlock(cursorPos);
|
||||||
|
QString lineText = block.text();
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (QChar c : lineText) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else if (c == '\t') leadingSpaces += 4;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (leadingSpaces > 0) {
|
||||||
|
systemPrompt += QString("\n- CRITICAL: Current line has %1 spaces of indentation"
|
||||||
|
"\n- If generating multiline code, EVERY line must start with at least %1 spaces"
|
||||||
|
"\n- If generating single-line code, it will be inserted inline (no indentation needed)")
|
||||||
|
.arg(leadingSpaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
||||||
|
"\n- Maintain consistent indentation for nested blocks"
|
||||||
|
"\n- Do NOT remove or reduce the base indentation level"
|
||||||
|
"\n\n## Code Style:"
|
||||||
|
"\n- Match the coding style of the surrounding code (naming, spacing, braces, etc.)"
|
||||||
|
"\n- Preserve the original code structure when possible"
|
||||||
|
"\n- Only change what is necessary to fulfill the user's request";
|
||||||
|
|
||||||
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
|
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
|
||||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||||
@ -338,19 +387,7 @@ void QuickRefactorHandler::handleLLMResponse(
|
|||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
|
QString cleanedResponse = LLMCore::ResponseCleaner::clean(response);
|
||||||
QString cleanedResponse = response.trimmed();
|
|
||||||
if (cleanedResponse.startsWith("```")) {
|
|
||||||
int firstNewLine = cleanedResponse.indexOf('\n');
|
|
||||||
int lastFence = cleanedResponse.lastIndexOf("```");
|
|
||||||
|
|
||||||
if (firstNewLine != -1 && lastFence > firstNewLine) {
|
|
||||||
cleanedResponse
|
|
||||||
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
|
|
||||||
} else if (lastFence != -1) {
|
|
||||||
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.newText = cleanedResponse;
|
result.newText = cleanedResponse;
|
||||||
|
|||||||
115
RefactorContextHelper.hpp
Normal file
115
RefactorContextHelper.hpp
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QString>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextBlock>
|
||||||
|
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
struct RefactorContext
|
||||||
|
{
|
||||||
|
QString originalText;
|
||||||
|
QString textBeforeCursor;
|
||||||
|
QString textAfterCursor;
|
||||||
|
QString contextBefore;
|
||||||
|
QString contextAfter;
|
||||||
|
int startPos{0};
|
||||||
|
int endPos{0};
|
||||||
|
bool isInsertion{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
class RefactorContextHelper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static RefactorContext extractContext(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
int contextLinesBefore = 3,
|
||||||
|
int contextLinesAfter = 3)
|
||||||
|
{
|
||||||
|
RefactorContext ctx;
|
||||||
|
if (!editor) {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocument *doc = editor->document();
|
||||||
|
ctx.startPos = range.begin.toPositionInDocument(doc);
|
||||||
|
ctx.endPos = range.end.toPositionInDocument(doc);
|
||||||
|
ctx.isInsertion = (ctx.startPos == ctx.endPos);
|
||||||
|
|
||||||
|
if (!ctx.isInsertion) {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
cursor.setPosition(ctx.endPos, QTextCursor::KeepAnchor);
|
||||||
|
ctx.originalText = cursor.selectedText();
|
||||||
|
ctx.originalText.replace(QChar(0x2029), "\n");
|
||||||
|
} else {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
|
||||||
|
int posInBlock = cursor.positionInBlock();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, posInBlock);
|
||||||
|
ctx.textBeforeCursor = cursor.selectedText();
|
||||||
|
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
ctx.textAfterCursor = cursor.selectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.contextBefore = extractContextLines(doc, ctx.startPos, contextLinesBefore, true);
|
||||||
|
ctx.contextAfter = extractContextLines(doc, ctx.endPos, contextLinesAfter, false);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString extractContextLines(QTextDocument *doc, int position, int lineCount, bool before)
|
||||||
|
{
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
QTextBlock currentBlock = cursor.block();
|
||||||
|
|
||||||
|
QStringList lines;
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
QTextBlock block = currentBlock.previous();
|
||||||
|
for (int i = 0; i < lineCount && block.isValid(); ++i) {
|
||||||
|
lines.prepend(block.text());
|
||||||
|
block = block.previous();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QTextBlock block = currentBlock.next();
|
||||||
|
for (int i = 0; i < lineCount && block.isValid(); ++i) {
|
||||||
|
lines.append(block.text());
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ add_library(LLMCore STATIC
|
|||||||
BaseTool.hpp BaseTool.cpp
|
BaseTool.hpp BaseTool.cpp
|
||||||
ContentBlocks.hpp
|
ContentBlocks.hpp
|
||||||
RulesLoader.hpp RulesLoader.cpp
|
RulesLoader.hpp RulesLoader.cpp
|
||||||
|
ResponseCleaner.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(LLMCore
|
target_link_libraries(LLMCore
|
||||||
|
|||||||
119
llmcore/ResponseCleaner.hpp
Normal file
119
llmcore/ResponseCleaner.hpp
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
namespace QodeAssist::LLMCore {
|
||||||
|
|
||||||
|
class ResponseCleaner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static QString clean(const QString &response)
|
||||||
|
{
|
||||||
|
QString cleaned = removeCodeBlocks(response);
|
||||||
|
cleaned = trimWhitespace(cleaned);
|
||||||
|
cleaned = removeExplanations(cleaned);
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString removeCodeBlocks(const QString &text)
|
||||||
|
{
|
||||||
|
if (!text.contains("```")) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegularExpression codeBlockRegex("```\\w*\\n([\\s\\S]*?)```");
|
||||||
|
QRegularExpressionMatch match = codeBlockRegex.match(text);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
return match.captured(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int firstFence = text.indexOf("```");
|
||||||
|
int lastFence = text.lastIndexOf("```");
|
||||||
|
if (firstFence != -1 && lastFence > firstFence) {
|
||||||
|
int firstNewLine = text.indexOf('\n', firstFence);
|
||||||
|
if (firstNewLine != -1) {
|
||||||
|
return text.mid(firstNewLine + 1, lastFence - firstNewLine - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString trimWhitespace(const QString &text)
|
||||||
|
{
|
||||||
|
QString result = text;
|
||||||
|
while (result.startsWith('\n') || result.startsWith('\r')) {
|
||||||
|
result = result.mid(1);
|
||||||
|
}
|
||||||
|
while (result.endsWith('\n') || result.endsWith('\r')) {
|
||||||
|
result.chop(1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString removeExplanations(const QString &text)
|
||||||
|
{
|
||||||
|
static const QStringList explanationPrefixes = {
|
||||||
|
"here's the", "here is the", "here's", "here is",
|
||||||
|
"the refactored", "refactored code:", "code:",
|
||||||
|
"i've refactored", "i refactored", "i've changed", "i changed"
|
||||||
|
};
|
||||||
|
|
||||||
|
QStringList lines = text.split('\n');
|
||||||
|
int startLine = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < qMin(3, lines.size()); ++i) {
|
||||||
|
QString line = lines[i].trimmed().toLower();
|
||||||
|
bool isExplanation = false;
|
||||||
|
|
||||||
|
for (const QString &prefix : explanationPrefixes) {
|
||||||
|
if (line.startsWith(prefix) || line.contains(prefix + " code")) {
|
||||||
|
isExplanation = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.length() < 50 && line.endsWith(':')) {
|
||||||
|
isExplanation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExplanation) {
|
||||||
|
startLine = i + 1;
|
||||||
|
} else if (!line.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startLine > 0 && startLine < lines.size()) {
|
||||||
|
lines = lines.mid(startLine);
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::LLMCore
|
||||||
|
|
||||||
@ -155,6 +155,50 @@ QuickRefactorSettings::QuickRefactorSettings()
|
|||||||
readStringsAfterCursor.setRange(0, 10000);
|
readStringsAfterCursor.setRange(0, 10000);
|
||||||
readStringsAfterCursor.setDefaultValue(30);
|
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);
|
||||||
|
|
||||||
|
widgetOrientation.setSettingsKey(Constants::QR_WIDGET_ORIENTATION);
|
||||||
|
widgetOrientation.setLabelText(Tr::tr("Widget Orientation:"));
|
||||||
|
widgetOrientation.setToolTip(
|
||||||
|
Tr::tr("Choose default orientation for refactor widget:\n"
|
||||||
|
"- Horizontal: Original and refactored code side by side (default)\n"
|
||||||
|
"- Vertical: Original and refactored code stacked vertically"));
|
||||||
|
widgetOrientation.addOption(Tr::tr("Horizontal"));
|
||||||
|
widgetOrientation.addOption(Tr::tr("Vertical"));
|
||||||
|
widgetOrientation.setDefaultValue(0);
|
||||||
|
|
||||||
|
widgetMinWidth.setSettingsKey(Constants::QR_WIDGET_MIN_WIDTH);
|
||||||
|
widgetMinWidth.setLabelText(Tr::tr("Widget Minimum Width:"));
|
||||||
|
widgetMinWidth.setToolTip(Tr::tr("Minimum width for the refactor widget (in pixels)"));
|
||||||
|
widgetMinWidth.setRange(400, 2000);
|
||||||
|
widgetMinWidth.setDefaultValue(600);
|
||||||
|
|
||||||
|
widgetMaxWidth.setSettingsKey(Constants::QR_WIDGET_MAX_WIDTH);
|
||||||
|
widgetMaxWidth.setLabelText(Tr::tr("Widget Maximum Width:"));
|
||||||
|
widgetMaxWidth.setToolTip(Tr::tr("Maximum width for the refactor widget (in pixels)"));
|
||||||
|
widgetMaxWidth.setRange(600, 3000);
|
||||||
|
widgetMaxWidth.setDefaultValue(1400);
|
||||||
|
|
||||||
|
widgetMinHeight.setSettingsKey(Constants::QR_WIDGET_MIN_HEIGHT);
|
||||||
|
widgetMinHeight.setLabelText(Tr::tr("Widget Minimum Height:"));
|
||||||
|
widgetMinHeight.setToolTip(Tr::tr("Minimum height for the refactor widget (in pixels)"));
|
||||||
|
widgetMinHeight.setRange(80, 800);
|
||||||
|
widgetMinHeight.setDefaultValue(120);
|
||||||
|
|
||||||
|
widgetMaxHeight.setSettingsKey(Constants::QR_WIDGET_MAX_HEIGHT);
|
||||||
|
widgetMaxHeight.setLabelText(Tr::tr("Widget Maximum Height:"));
|
||||||
|
widgetMaxHeight.setToolTip(Tr::tr("Maximum height for the refactor widget (in pixels)"));
|
||||||
|
widgetMaxHeight.setRange(200, 1200);
|
||||||
|
widgetMaxHeight.setDefaultValue(500);
|
||||||
|
|
||||||
systemPrompt.setSettingsKey(Constants::QR_SYSTEM_PROMPT);
|
systemPrompt.setSettingsKey(Constants::QR_SYSTEM_PROMPT);
|
||||||
systemPrompt.setLabelText(Tr::tr("System Prompt:"));
|
systemPrompt.setLabelText(Tr::tr("System Prompt:"));
|
||||||
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||||
@ -198,6 +242,12 @@ QuickRefactorSettings::QuickRefactorSettings()
|
|||||||
contextGrid.addRow({Row{readFullFile}});
|
contextGrid.addRow({Row{readFullFile}});
|
||||||
contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}});
|
contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}});
|
||||||
|
|
||||||
|
auto displayGrid = Grid{};
|
||||||
|
displayGrid.addRow({Row{displayMode}});
|
||||||
|
displayGrid.addRow({Row{widgetOrientation}});
|
||||||
|
displayGrid.addRow({Row{widgetMinWidth, widgetMaxWidth}});
|
||||||
|
displayGrid.addRow({Row{widgetMinHeight, widgetMaxHeight}});
|
||||||
|
|
||||||
return Column{
|
return Column{
|
||||||
Row{Stretch{1}, resetToDefaults},
|
Row{Stretch{1}, resetToDefaults},
|
||||||
Space{8},
|
Space{8},
|
||||||
@ -212,6 +262,8 @@ QuickRefactorSettings::QuickRefactorSettings()
|
|||||||
Space{8},
|
Space{8},
|
||||||
Group{title(Tr::tr("Context Settings")), Column{Row{contextGrid, Stretch{1}}}},
|
Group{title(Tr::tr("Context Settings")), Column{Row{contextGrid, Stretch{1}}}},
|
||||||
Space{8},
|
Space{8},
|
||||||
|
Group{title(Tr::tr("Display Settings")), Column{Row{displayGrid, Stretch{1}}}},
|
||||||
|
Space{8},
|
||||||
Group{title(Tr::tr("Prompt Settings")), Column{Row{systemPrompt}}},
|
Group{title(Tr::tr("Prompt Settings")), Column{Row{systemPrompt}}},
|
||||||
Space{8},
|
Space{8},
|
||||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||||
@ -240,6 +292,32 @@ void QuickRefactorSettings::setupConnections()
|
|||||||
writeSettings();
|
writeSettings();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enable/disable widgetOrientation based on displayMode
|
||||||
|
// 0 = Inline Widget, 1 = Qt Creator Suggestion
|
||||||
|
auto updateWidgetOrientationEnabled = [this]() {
|
||||||
|
bool isInlineWidget = (displayMode.volatileValue() == 0);
|
||||||
|
widgetOrientation.setEnabled(isInlineWidget);
|
||||||
|
};
|
||||||
|
|
||||||
|
connect(&displayMode, &Utils::SelectionAspect::volatileValueChanged,
|
||||||
|
this, updateWidgetOrientationEnabled);
|
||||||
|
|
||||||
|
updateWidgetOrientationEnabled();
|
||||||
|
|
||||||
|
auto validateWidgetSizes = [this]() {
|
||||||
|
if (widgetMinWidth.volatileValue() > widgetMaxWidth.volatileValue()) {
|
||||||
|
widgetMaxWidth.setValue(widgetMinWidth.volatileValue());
|
||||||
|
}
|
||||||
|
if (widgetMinHeight.volatileValue() > widgetMaxHeight.volatileValue()) {
|
||||||
|
widgetMaxHeight.setValue(widgetMinHeight.volatileValue());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
connect(&widgetMinWidth, &Utils::IntegerAspect::volatileValueChanged, this, validateWidgetSizes);
|
||||||
|
connect(&widgetMaxWidth, &Utils::IntegerAspect::volatileValueChanged, this, validateWidgetSizes);
|
||||||
|
connect(&widgetMinHeight, &Utils::IntegerAspect::volatileValueChanged, this, validateWidgetSizes);
|
||||||
|
connect(&widgetMaxHeight, &Utils::IntegerAspect::volatileValueChanged, this, validateWidgetSizes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorSettings::resetSettingsToDefaults()
|
void QuickRefactorSettings::resetSettingsToDefaults()
|
||||||
@ -272,6 +350,12 @@ void QuickRefactorSettings::resetSettingsToDefaults()
|
|||||||
resetAspect(readFileParts);
|
resetAspect(readFileParts);
|
||||||
resetAspect(readStringsBeforeCursor);
|
resetAspect(readStringsBeforeCursor);
|
||||||
resetAspect(readStringsAfterCursor);
|
resetAspect(readStringsAfterCursor);
|
||||||
|
resetAspect(displayMode);
|
||||||
|
resetAspect(widgetOrientation);
|
||||||
|
resetAspect(widgetMinWidth);
|
||||||
|
resetAspect(widgetMaxWidth);
|
||||||
|
resetAspect(widgetMinHeight);
|
||||||
|
resetAspect(widgetMaxHeight);
|
||||||
resetAspect(systemPrompt);
|
resetAspect(systemPrompt);
|
||||||
writeSettings();
|
writeSettings();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,6 +67,14 @@ public:
|
|||||||
Utils::IntegerAspect readStringsBeforeCursor{this};
|
Utils::IntegerAspect readStringsBeforeCursor{this};
|
||||||
Utils::IntegerAspect readStringsAfterCursor{this};
|
Utils::IntegerAspect readStringsAfterCursor{this};
|
||||||
|
|
||||||
|
// Display Settings
|
||||||
|
Utils::SelectionAspect displayMode{this};
|
||||||
|
Utils::SelectionAspect widgetOrientation{this};
|
||||||
|
Utils::IntegerAspect widgetMinWidth{this};
|
||||||
|
Utils::IntegerAspect widgetMaxWidth{this};
|
||||||
|
Utils::IntegerAspect widgetMinHeight{this};
|
||||||
|
Utils::IntegerAspect widgetMaxHeight{this};
|
||||||
|
|
||||||
// Prompt Settings
|
// Prompt Settings
|
||||||
Utils::StringAspect systemPrompt{this};
|
Utils::StringAspect systemPrompt{this};
|
||||||
|
|
||||||
|
|||||||
@ -220,5 +220,11 @@ const char QR_READ_FULL_FILE[] = "QodeAssist.qrReadFullFile";
|
|||||||
const char QR_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.qrReadStringsBeforeCursor";
|
const char QR_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.qrReadStringsBeforeCursor";
|
||||||
const char QR_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.qrReadStringsAfterCursor";
|
const char QR_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.qrReadStringsAfterCursor";
|
||||||
const char QR_SYSTEM_PROMPT[] = "QodeAssist.qrSystemPrompt";
|
const char QR_SYSTEM_PROMPT[] = "QodeAssist.qrSystemPrompt";
|
||||||
|
const char QR_DISPLAY_MODE[] = "QodeAssist.qrDisplayMode";
|
||||||
|
const char QR_WIDGET_ORIENTATION[] = "QodeAssist.qrWidgetOrientation";
|
||||||
|
const char QR_WIDGET_MIN_WIDTH[] = "QodeAssist.qrWidgetMinWidth";
|
||||||
|
const char QR_WIDGET_MAX_WIDTH[] = "QodeAssist.qrWidgetMaxWidth";
|
||||||
|
const char QR_WIDGET_MIN_HEIGHT[] = "QodeAssist.qrWidgetMinHeight";
|
||||||
|
const char QR_WIDGET_MAX_HEIGHT[] = "QodeAssist.qrWidgetMaxHeight";
|
||||||
|
|
||||||
} // namespace QodeAssist::Constants
|
} // namespace QodeAssist::Constants
|
||||||
|
|||||||
125
widgets/ContextExtractor.hpp
Normal file
125
widgets/ContextExtractor.hpp
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QTextDocument>
|
||||||
|
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class ContextExtractor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static QString extractBefore(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
int lineCount)
|
||||||
|
{
|
||||||
|
if (!editor || lineCount <= 0) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocument *doc = editor->document();
|
||||||
|
int startLine = range.begin.line;
|
||||||
|
int contextStartLine = qMax(1, startLine - lineCount);
|
||||||
|
|
||||||
|
QStringList contextLines;
|
||||||
|
for (int line = contextStartLine; line < startLine; ++line) {
|
||||||
|
QTextBlock block = doc->findBlockByNumber(line - 1);
|
||||||
|
if (block.isValid()) {
|
||||||
|
contextLines.append(block.text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextLines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString extractAfter(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
int lineCount)
|
||||||
|
{
|
||||||
|
if (!editor || lineCount <= 0) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocument *doc = editor->document();
|
||||||
|
int endLine = range.end.line;
|
||||||
|
int totalLines = doc->blockCount();
|
||||||
|
int contextEndLine = qMin(totalLines, endLine + lineCount);
|
||||||
|
|
||||||
|
QStringList contextLines;
|
||||||
|
for (int line = endLine + 1; line <= contextEndLine; ++line) {
|
||||||
|
QTextBlock block = doc->findBlockByNumber(line - 1);
|
||||||
|
if (block.isValid()) {
|
||||||
|
contextLines.append(block.text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextLines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString extractLineContext(QTextDocument *doc, int position, bool before)
|
||||||
|
{
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
int posInBlock = cursor.positionInBlock();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, posInBlock);
|
||||||
|
return cursor.selectedText();
|
||||||
|
} else {
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
return cursor.selectedText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QStringList extractSurroundingLines(QTextDocument *doc, int position, int linesBefore, int linesAfter)
|
||||||
|
{
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
QTextBlock currentBlock = cursor.block();
|
||||||
|
|
||||||
|
QStringList result;
|
||||||
|
|
||||||
|
QTextBlock blockBefore = currentBlock.previous();
|
||||||
|
QStringList beforeLines;
|
||||||
|
for (int i = 0; i < linesBefore && blockBefore.isValid(); ++i) {
|
||||||
|
beforeLines.prepend(blockBefore.text());
|
||||||
|
blockBefore = blockBefore.previous();
|
||||||
|
}
|
||||||
|
result.append(beforeLines);
|
||||||
|
|
||||||
|
QTextBlock blockAfter = currentBlock.next();
|
||||||
|
for (int i = 0; i < linesAfter && blockAfter.isValid(); ++i) {
|
||||||
|
result.append(blockAfter.text());
|
||||||
|
blockAfter = blockAfter.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
71
widgets/DiffStatistics.hpp
Normal file
71
widgets/DiffStatistics.hpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QString>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#include <utils/differ.h>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class DiffStatistics
|
||||||
|
{
|
||||||
|
Q_DECLARE_TR_FUNCTIONS(DiffStatistics)
|
||||||
|
|
||||||
|
public:
|
||||||
|
DiffStatistics() = default;
|
||||||
|
|
||||||
|
void calculate(const QList<Utils::Diff> &diffList)
|
||||||
|
{
|
||||||
|
m_linesAdded = 0;
|
||||||
|
m_linesRemoved = 0;
|
||||||
|
|
||||||
|
for (const auto &diff : diffList) {
|
||||||
|
if (diff.command == Utils::Diff::Insert) {
|
||||||
|
m_linesAdded += diff.text.count('\n') + (diff.text.isEmpty() ? 0 : 1);
|
||||||
|
} else if (diff.command == Utils::Diff::Delete) {
|
||||||
|
m_linesRemoved += diff.text.count('\n') + (diff.text.isEmpty() ? 0 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int linesAdded() const { return m_linesAdded; }
|
||||||
|
int linesRemoved() const { return m_linesRemoved; }
|
||||||
|
|
||||||
|
QString formatSummary() const
|
||||||
|
{
|
||||||
|
if (m_linesAdded > 0 && m_linesRemoved > 0) {
|
||||||
|
return tr("+%1 lines, -%2 lines").arg(m_linesAdded).arg(m_linesRemoved);
|
||||||
|
} else if (m_linesAdded > 0) {
|
||||||
|
return tr("+%1 lines").arg(m_linesAdded);
|
||||||
|
} else if (m_linesRemoved > 0) {
|
||||||
|
return tr("-%1 lines").arg(m_linesRemoved);
|
||||||
|
}
|
||||||
|
return tr("No changes");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_linesAdded{0};
|
||||||
|
int m_linesRemoved{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
732
widgets/RefactorWidget.cpp
Normal file
732
widgets/RefactorWidget.cpp
Normal file
@ -0,0 +1,732 @@
|
|||||||
|
/*
|
||||||
|
* 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 "DiffStatistics.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 <coreplugin/icore.h>
|
||||||
|
#include <utils/differ.h>
|
||||||
|
#include <utils/theme/theme.h>
|
||||||
|
|
||||||
|
#include "settings/QuickRefactorSettings.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
CustomSplitterHandle::CustomSplitterHandle(Qt::Orientation orientation, QSplitter *parent)
|
||||||
|
: QSplitterHandle(orientation, parent)
|
||||||
|
{
|
||||||
|
if (orientation == Qt::Horizontal) {
|
||||||
|
setCursor(Qt::SplitHCursor);
|
||||||
|
} else {
|
||||||
|
setCursor(Qt::SplitVCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMouseTracking(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomSplitterHandle::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
QColor bgColor = Utils::creatorColor(Utils::Theme::BackgroundColorHover);
|
||||||
|
bgColor.setAlpha(m_hovered ? 150 : 50);
|
||||||
|
painter.fillRect(rect(), bgColor);
|
||||||
|
|
||||||
|
QColor lineColor = Utils::creatorColor(Utils::Theme::SplitterColor);
|
||||||
|
lineColor.setAlpha(m_hovered ? 255 : 180);
|
||||||
|
|
||||||
|
const int lineWidth = m_hovered ? 3 : 2;
|
||||||
|
const int margin = 10;
|
||||||
|
painter.setPen(QPen(lineColor, lineWidth));
|
||||||
|
|
||||||
|
if (orientation() == Qt::Horizontal) {
|
||||||
|
int x = width() / 2;
|
||||||
|
painter.drawLine(x, margin, x, height() - margin);
|
||||||
|
|
||||||
|
painter.setBrush(lineColor);
|
||||||
|
int centerY = height() / 2;
|
||||||
|
const int dotSize = m_hovered ? 3 : 2;
|
||||||
|
const int dotSpacing = 8;
|
||||||
|
for (int i = -2; i <= 2; ++i) {
|
||||||
|
painter.drawEllipse(QPoint(x, centerY + i * dotSpacing), dotSize, dotSize);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int y = height() / 2;
|
||||||
|
painter.drawLine(margin, y, width() - margin, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomSplitterHandle::enterEvent(QEnterEvent *event)
|
||||||
|
{
|
||||||
|
m_hovered = true;
|
||||||
|
update();
|
||||||
|
QSplitterHandle::enterEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomSplitterHandle::leaveEvent(QEvent *event)
|
||||||
|
{
|
||||||
|
m_hovered = false;
|
||||||
|
update();
|
||||||
|
QSplitterHandle::leaveEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSplitter::CustomSplitter(Qt::Orientation orientation, QWidget *parent)
|
||||||
|
: QSplitter(orientation, parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QSplitterHandle *CustomSplitter::createHandle()
|
||||||
|
{
|
||||||
|
return new CustomSplitterHandle(orientation(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefactorWidget::RefactorWidget(TextEditor::TextEditorWidget *sourceEditor, QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_sourceEditor(sourceEditor)
|
||||||
|
, m_leftEditor(nullptr)
|
||||||
|
, m_rightEditor(nullptr)
|
||||||
|
, m_leftContainer(nullptr)
|
||||||
|
, m_splitter(nullptr)
|
||||||
|
, m_statsLabel(nullptr)
|
||||||
|
, m_applyButton(nullptr)
|
||||||
|
, m_declineButton(nullptr)
|
||||||
|
, m_editorWidth(800)
|
||||||
|
, m_syncingScroll(false)
|
||||||
|
, m_isClosing(false)
|
||||||
|
, m_linesAdded(0)
|
||||||
|
, m_linesRemoved(0)
|
||||||
|
{
|
||||||
|
setupUi();
|
||||||
|
applyEditorSettings();
|
||||||
|
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefactorWidget::~RefactorWidget()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::setupUi()
|
||||||
|
{
|
||||||
|
auto *mainLayout = new QVBoxLayout(this);
|
||||||
|
mainLayout->setContentsMargins(6, 6, 6, 6);
|
||||||
|
mainLayout->setSpacing(4);
|
||||||
|
|
||||||
|
m_statsLabel = new QLabel(this);
|
||||||
|
m_statsLabel->setAlignment(Qt::AlignLeft);
|
||||||
|
mainLayout->addWidget(m_statsLabel);
|
||||||
|
|
||||||
|
m_leftDocument = QSharedPointer<TextEditor::TextDocument>::create();
|
||||||
|
m_rightDocument = QSharedPointer<TextEditor::TextDocument>::create();
|
||||||
|
|
||||||
|
Qt::Orientation initialOrientation = Settings::quickRefactorSettings().widgetOrientation.value() == 1
|
||||||
|
? Qt::Vertical : Qt::Horizontal;
|
||||||
|
|
||||||
|
m_splitter = new CustomSplitter(initialOrientation, this);
|
||||||
|
m_splitter->setChildrenCollapsible(false);
|
||||||
|
m_splitter->setHandleWidth(12);
|
||||||
|
m_splitter->setStyleSheet("QSplitter::handle { background-color: transparent; }");
|
||||||
|
|
||||||
|
m_leftEditor = new TextEditor::TextEditorWidget();
|
||||||
|
m_leftEditor->setTextDocument(m_leftDocument);
|
||||||
|
m_leftEditor->setReadOnly(true);
|
||||||
|
m_leftEditor->setFrameStyle(QFrame::StyledPanel);
|
||||||
|
m_leftEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
m_leftEditor->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
|
m_leftEditor->setMinimumHeight(100);
|
||||||
|
m_leftEditor->setMinimumWidth(150);
|
||||||
|
m_leftEditor->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
|
m_leftEditor->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
|
||||||
|
|
||||||
|
m_rightEditor = new TextEditor::TextEditorWidget();
|
||||||
|
m_rightEditor->setTextDocument(m_rightDocument);
|
||||||
|
m_rightEditor->setReadOnly(false);
|
||||||
|
m_rightEditor->setFrameStyle(QFrame::StyledPanel);
|
||||||
|
m_rightEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
|
m_rightEditor->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
|
m_rightEditor->setMinimumHeight(100);
|
||||||
|
m_rightEditor->setMinimumWidth(150);
|
||||||
|
m_rightEditor->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
|
|
||||||
|
m_leftContainer = new QWidget();
|
||||||
|
m_leftContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
|
auto *leftLayout = new QVBoxLayout(m_leftContainer);
|
||||||
|
leftLayout->setSpacing(2);
|
||||||
|
leftLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
||||||
|
auto *originalLabel = new QLabel(tr("◄ Original"), m_leftContainer);
|
||||||
|
leftLayout->addWidget(originalLabel);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
m_applyButton = new QPushButton(tr("✓ Apply (⌘+Enter)"), this);
|
||||||
|
#else
|
||||||
|
m_applyButton = new QPushButton(tr("✓ Apply (Ctrl+Enter)"), this);
|
||||||
|
#endif
|
||||||
|
m_applyButton->setFocusPolicy(Qt::NoFocus);
|
||||||
|
m_applyButton->setCursor(Qt::PointingHandCursor);
|
||||||
|
m_applyButton->setMaximumHeight(24);
|
||||||
|
|
||||||
|
m_declineButton = new QPushButton(tr("✗ Decline (Esc)"), this);
|
||||||
|
m_declineButton->setFocusPolicy(Qt::NoFocus);
|
||||||
|
m_declineButton->setCursor(Qt::PointingHandCursor);
|
||||||
|
m_declineButton->setMaximumHeight(24);
|
||||||
|
|
||||||
|
buttonLayout->addStretch();
|
||||||
|
buttonLayout->addWidget(m_applyButton);
|
||||||
|
buttonLayout->addWidget(m_declineButton);
|
||||||
|
|
||||||
|
mainLayout->addLayout(buttonLayout);
|
||||||
|
|
||||||
|
connect(m_applyButton, &QPushButton::clicked, this, &RefactorWidget::applyRefactoring);
|
||||||
|
connect(m_declineButton, &QPushButton::clicked, this, &RefactorWidget::declineRefactoring);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::setDiffContent(const QString &originalText, const QString &refactoredText)
|
||||||
|
{
|
||||||
|
setDiffContent(originalText, refactoredText, QString(), QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::setDiffContent(const QString &originalText, const QString &refactoredText,
|
||||||
|
const QString &contextBefore, const QString &contextAfter)
|
||||||
|
{
|
||||||
|
m_originalText = originalText;
|
||||||
|
m_refactoredText = refactoredText;
|
||||||
|
m_contextBefore = contextBefore;
|
||||||
|
m_contextAfter = contextAfter;
|
||||||
|
|
||||||
|
m_leftContainer->setVisible(true);
|
||||||
|
|
||||||
|
QString leftFullText;
|
||||||
|
QString rightFullText;
|
||||||
|
|
||||||
|
if (!contextBefore.isEmpty()) {
|
||||||
|
leftFullText = contextBefore + "\n";
|
||||||
|
rightFullText = contextBefore + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
leftFullText += originalText;
|
||||||
|
rightFullText += refactoredText;
|
||||||
|
|
||||||
|
if (!contextAfter.isEmpty()) {
|
||||||
|
leftFullText += "\n" + contextAfter;
|
||||||
|
rightFullText += "\n" + contextAfter;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_leftDocument->setPlainText(leftFullText);
|
||||||
|
m_rightDocument->setPlainText(rightFullText);
|
||||||
|
|
||||||
|
applySyntaxHighlighting();
|
||||||
|
|
||||||
|
if (!contextBefore.isEmpty() || !contextAfter.isEmpty()) {
|
||||||
|
dimContextLines(contextBefore, contextAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils::Differ differ;
|
||||||
|
m_cachedDiffList = differ.diff(m_originalText, m_refactoredText);
|
||||||
|
|
||||||
|
highlightDifferences();
|
||||||
|
addLineMarkers();
|
||||||
|
|
||||||
|
calculateStats();
|
||||||
|
updateStatsLabel();
|
||||||
|
|
||||||
|
updateSizeToContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::highlightDifferences()
|
||||||
|
{
|
||||||
|
if (m_cachedDiffList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Utils::Diff> leftDiffs;
|
||||||
|
QList<Utils::Diff> rightDiffs;
|
||||||
|
Utils::Differ::splitDiffList(m_cachedDiffList, &leftDiffs, &rightDiffs);
|
||||||
|
|
||||||
|
int contextBeforeOffset = m_contextBefore.isEmpty() ? 0 : (m_contextBefore.length() + 1);
|
||||||
|
|
||||||
|
QColor normalTextColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
|
||||||
|
|
||||||
|
QTextCursor leftCursor(m_leftDocument->document());
|
||||||
|
QTextCharFormat removedFormat;
|
||||||
|
QColor removedBg = Utils::creatorColor(Utils::Theme::TextColorError);
|
||||||
|
removedBg.setAlpha(30);
|
||||||
|
removedFormat.setBackground(removedBg);
|
||||||
|
removedFormat.setForeground(normalTextColor);
|
||||||
|
|
||||||
|
int leftPos = 0;
|
||||||
|
for (const auto &diff : leftDiffs) {
|
||||||
|
if (diff.command == Utils::Diff::Delete) {
|
||||||
|
leftCursor.setPosition(contextBeforeOffset + leftPos);
|
||||||
|
leftCursor.setPosition(contextBeforeOffset + leftPos + diff.text.length(), QTextCursor::KeepAnchor);
|
||||||
|
leftCursor.setCharFormat(removedFormat);
|
||||||
|
}
|
||||||
|
if (diff.command != Utils::Diff::Insert) {
|
||||||
|
leftPos += diff.text.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor rightCursor(m_rightDocument->document());
|
||||||
|
QTextCharFormat addedFormat;
|
||||||
|
QColor addedBg = Utils::creatorColor(Utils::Theme::IconsRunColor);
|
||||||
|
addedBg.setAlpha(60);
|
||||||
|
addedFormat.setBackground(addedBg);
|
||||||
|
addedFormat.setForeground(normalTextColor);
|
||||||
|
|
||||||
|
int rightPos = 0;
|
||||||
|
for (const auto &diff : rightDiffs) {
|
||||||
|
if (diff.command == Utils::Diff::Insert) {
|
||||||
|
rightCursor.setPosition(contextBeforeOffset + rightPos);
|
||||||
|
rightCursor.setPosition(contextBeforeOffset + rightPos + diff.text.length(), QTextCursor::KeepAnchor);
|
||||||
|
rightCursor.setCharFormat(addedFormat);
|
||||||
|
}
|
||||||
|
if (diff.command != Utils::Diff::Delete) {
|
||||||
|
rightPos += diff.text.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::dimContextLines(const QString &contextBefore, const QString &contextAfter)
|
||||||
|
{
|
||||||
|
QTextCharFormat dimFormat;
|
||||||
|
dimFormat.setForeground(Utils::creatorColor(Utils::Theme::TextColorDisabled));
|
||||||
|
|
||||||
|
auto dimLines = [&](QTextDocument *doc, int lineCount, bool fromStart) {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
if (!fromStart) {
|
||||||
|
cursor.movePosition(QTextCursor::End);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < lineCount; ++i) {
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
cursor.setCharFormat(dimFormat);
|
||||||
|
|
||||||
|
if (fromStart) {
|
||||||
|
if (!cursor.block().isValid() || !cursor.movePosition(QTextCursor::NextBlock)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!cursor.movePosition(QTextCursor::PreviousBlock)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!contextBefore.isEmpty()) {
|
||||||
|
int lines = contextBefore.count('\n') + (contextBefore.endsWith('\n') ? 0 : 1);
|
||||||
|
dimLines(m_leftDocument->document(), lines, true);
|
||||||
|
dimLines(m_rightDocument->document(), lines, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contextAfter.isEmpty()) {
|
||||||
|
int lines = contextAfter.count('\n') + (contextAfter.endsWith('\n') ? 0 : 1);
|
||||||
|
dimLines(m_leftDocument->document(), lines, false);
|
||||||
|
dimLines(m_rightDocument->document(), lines, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RefactorWidget::getRefactoredText() const
|
||||||
|
{
|
||||||
|
return m_applyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::setRange(const Utils::Text::Range &range)
|
||||||
|
{
|
||||||
|
m_range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::setEditorWidth(int width)
|
||||||
|
{
|
||||||
|
m_editorWidth = width;
|
||||||
|
updateSizeToContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::setApplyCallback(std::function<void(const QString &)> callback)
|
||||||
|
{
|
||||||
|
m_applyCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::setDeclineCallback(std::function<void()> callback)
|
||||||
|
{
|
||||||
|
m_declineCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::applyRefactoring()
|
||||||
|
{
|
||||||
|
if (m_isClosing) return;
|
||||||
|
m_isClosing = true;
|
||||||
|
|
||||||
|
if (m_applyCallback) {
|
||||||
|
m_applyCallback(m_applyText);
|
||||||
|
}
|
||||||
|
emit applied();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::declineRefactoring()
|
||||||
|
{
|
||||||
|
if (m_isClosing) return;
|
||||||
|
m_isClosing = true;
|
||||||
|
|
||||||
|
if (m_declineCallback) {
|
||||||
|
m_declineCallback();
|
||||||
|
}
|
||||||
|
emit declined();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
const QColor bgColor = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
||||||
|
const QColor borderColor = Utils::creatorColor(Utils::Theme::SplitterColor);
|
||||||
|
|
||||||
|
painter.fillRect(rect(), bgColor);
|
||||||
|
painter.setPen(QPen(borderColor, 2));
|
||||||
|
painter.drawRoundedRect(rect().adjusted(2, 2, -2, -2), 6, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RefactorWidget::event(QEvent *event)
|
||||||
|
{
|
||||||
|
if (event->type() == QEvent::ShortcutOverride) {
|
||||||
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||||
|
|
||||||
|
if (((keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) &&
|
||||||
|
keyEvent->modifiers() == Qt::ControlModifier) ||
|
||||||
|
keyEvent->key() == Qt::Key_Escape) {
|
||||||
|
event->accept();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type() == QEvent::KeyPress) {
|
||||||
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||||
|
|
||||||
|
if ((keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) &&
|
||||||
|
keyEvent->modifiers() == Qt::ControlModifier) {
|
||||||
|
applyRefactoring();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyEvent->key() == Qt::Key_Escape) {
|
||||||
|
declineRefactoring();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QWidget::event(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::syncLeftScroll(int value)
|
||||||
|
{
|
||||||
|
if (m_syncingScroll) return;
|
||||||
|
m_syncingScroll = true;
|
||||||
|
m_rightEditor->verticalScrollBar()->setValue(value);
|
||||||
|
m_syncingScroll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::syncRightScroll(int value)
|
||||||
|
{
|
||||||
|
if (m_syncingScroll) return;
|
||||||
|
m_syncingScroll = true;
|
||||||
|
m_leftEditor->verticalScrollBar()->setValue(value);
|
||||||
|
m_syncingScroll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::syncLeftHorizontalScroll(int value)
|
||||||
|
{
|
||||||
|
if (m_syncingScroll) return;
|
||||||
|
m_syncingScroll = true;
|
||||||
|
m_rightEditor->horizontalScrollBar()->setValue(value);
|
||||||
|
m_syncingScroll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::syncRightHorizontalScroll(int value)
|
||||||
|
{
|
||||||
|
if (m_syncingScroll) return;
|
||||||
|
m_syncingScroll = true;
|
||||||
|
m_leftEditor->horizontalScrollBar()->setValue(value);
|
||||||
|
m_syncingScroll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::onRightEditorTextChanged()
|
||||||
|
{
|
||||||
|
QString fullText = m_rightDocument->plainText();
|
||||||
|
int startPos = m_contextBefore.isEmpty() ? 0 : m_contextBefore.length() + 1;
|
||||||
|
int endPos = m_contextAfter.isEmpty() ? fullText.length() : fullText.length() - m_contextAfter.length() - 1;
|
||||||
|
m_applyText = fullText.mid(startPos, endPos - startPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::closeEvent(QCloseEvent *event)
|
||||||
|
{
|
||||||
|
if (!m_isClosing) {
|
||||||
|
declineRefactoring();
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::calculateStats()
|
||||||
|
{
|
||||||
|
DiffStatistics stats;
|
||||||
|
stats.calculate(m_cachedDiffList);
|
||||||
|
m_linesAdded = stats.linesAdded();
|
||||||
|
m_linesRemoved = stats.linesRemoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::updateStatsLabel()
|
||||||
|
{
|
||||||
|
DiffStatistics stats;
|
||||||
|
stats.calculate(m_cachedDiffList);
|
||||||
|
m_statsLabel->setText("📊 " + stats.formatSummary());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::applySyntaxHighlighting()
|
||||||
|
{
|
||||||
|
if (!m_sourceEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *sourceDoc = m_sourceEditor->textDocument();
|
||||||
|
if (!sourceDoc || !sourceDoc->syntaxHighlighter()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_leftDocument->setMimeType(sourceDoc->mimeType());
|
||||||
|
m_rightDocument->setMimeType(sourceDoc->mimeType());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::addLineMarkers()
|
||||||
|
{
|
||||||
|
if (m_cachedDiffList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Utils::Diff> leftDiffs;
|
||||||
|
QList<Utils::Diff> rightDiffs;
|
||||||
|
Utils::Differ::splitDiffList(m_cachedDiffList, &leftDiffs, &rightDiffs);
|
||||||
|
|
||||||
|
int contextBeforeOffset = m_contextBefore.isEmpty() ? 0 : (m_contextBefore.length() + 1);
|
||||||
|
|
||||||
|
QColor removedMarker = Utils::creatorColor(Utils::Theme::TextColorError);
|
||||||
|
QColor addedMarker = Utils::creatorColor(Utils::Theme::IconsRunColor);
|
||||||
|
|
||||||
|
QTextCursor leftCursor(m_leftDocument->document());
|
||||||
|
int leftPos = 0;
|
||||||
|
|
||||||
|
for (const auto &diff : leftDiffs) {
|
||||||
|
if (diff.command == Utils::Diff::Delete) {
|
||||||
|
leftCursor.setPosition(contextBeforeOffset + leftPos);
|
||||||
|
leftCursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
leftCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
|
||||||
|
QTextBlockFormat blockFormat;
|
||||||
|
blockFormat.setBackground(QBrush(removedMarker.lighter(185)));
|
||||||
|
blockFormat.setLeftMargin(4);
|
||||||
|
blockFormat.setProperty(QTextFormat::FullWidthSelection, true);
|
||||||
|
|
||||||
|
leftCursor.setBlockFormat(blockFormat);
|
||||||
|
}
|
||||||
|
if (diff.command != Utils::Diff::Insert) {
|
||||||
|
leftPos += diff.text.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor rightCursor(m_rightDocument->document());
|
||||||
|
int rightPos = 0;
|
||||||
|
|
||||||
|
for (const auto &diff : rightDiffs) {
|
||||||
|
if (diff.command == Utils::Diff::Insert) {
|
||||||
|
rightCursor.setPosition(contextBeforeOffset + rightPos);
|
||||||
|
rightCursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
rightCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
|
||||||
|
QTextBlockFormat blockFormat;
|
||||||
|
blockFormat.setBackground(QBrush(addedMarker.lighter(195)));
|
||||||
|
blockFormat.setLeftMargin(4);
|
||||||
|
blockFormat.setProperty(QTextFormat::FullWidthSelection, true);
|
||||||
|
|
||||||
|
rightCursor.setBlockFormat(blockFormat);
|
||||||
|
}
|
||||||
|
if (diff.command != Utils::Diff::Delete) {
|
||||||
|
rightPos += diff.text.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::updateSizeToContent()
|
||||||
|
{
|
||||||
|
QFontMetrics fm(m_rightEditor->font());
|
||||||
|
int charWidth = fm.horizontalAdvance('m');
|
||||||
|
int lineHeight = fm.height();
|
||||||
|
int lineCount = m_rightDocument->document()->blockCount();
|
||||||
|
|
||||||
|
bool horizontal = m_splitter->orientation() == Qt::Horizontal;
|
||||||
|
|
||||||
|
const int minWidth = Settings::quickRefactorSettings().widgetMinWidth();
|
||||||
|
const int maxWidth = qMin(Settings::quickRefactorSettings().widgetMaxWidth(), m_editorWidth - 40);
|
||||||
|
const int minHeight = Settings::quickRefactorSettings().widgetMinHeight();
|
||||||
|
const int maxHeight = Settings::quickRefactorSettings().widgetMaxHeight();
|
||||||
|
|
||||||
|
if (horizontal) {
|
||||||
|
int totalWidth = qBound(minWidth, charWidth * 60 * 2 + 90, maxWidth);
|
||||||
|
setFixedWidth(totalWidth);
|
||||||
|
|
||||||
|
int editorHeight = qBound(minHeight, lineCount * lineHeight, maxHeight);
|
||||||
|
m_leftEditor->setMinimumHeight(editorHeight);
|
||||||
|
m_leftEditor->setMaximumHeight(editorHeight);
|
||||||
|
m_rightEditor->setMinimumHeight(editorHeight);
|
||||||
|
m_rightEditor->setMaximumHeight(editorHeight);
|
||||||
|
} else {
|
||||||
|
int editorWidth = qBound(minWidth, charWidth * 85 + 80, maxWidth);
|
||||||
|
setFixedWidth(editorWidth);
|
||||||
|
|
||||||
|
int editorHeight = qBound(minHeight, lineCount * lineHeight, maxHeight);
|
||||||
|
m_leftEditor->setMinimumHeight(editorHeight);
|
||||||
|
m_leftEditor->setMaximumHeight(editorHeight);
|
||||||
|
m_rightEditor->setMinimumHeight(editorHeight);
|
||||||
|
m_rightEditor->setMaximumHeight(editorHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGeometry();
|
||||||
|
adjustSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::applyEditorSettings()
|
||||||
|
{
|
||||||
|
if (!m_sourceEditor || !m_leftEditor || !m_rightEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont editorFont = m_sourceEditor->font();
|
||||||
|
m_leftEditor->setFont(editorFont);
|
||||||
|
m_rightEditor->setFont(editorFont);
|
||||||
|
|
||||||
|
QString labelStyle = QString("color: %1; padding: 2px 4px;")
|
||||||
|
.arg(Utils::creatorColor(Utils::Theme::TextColorDisabled).name());
|
||||||
|
|
||||||
|
for (auto *label : findChildren<QLabel *>()) {
|
||||||
|
if (label != m_statsLabel) {
|
||||||
|
QFont labelFont = label->font();
|
||||||
|
labelFont.setPointSize(qMax(8, editorFont.pointSize() - 2));
|
||||||
|
label->setFont(labelFont);
|
||||||
|
label->setStyleSheet(labelStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont statsFont = m_statsLabel->font();
|
||||||
|
statsFont.setBold(true);
|
||||||
|
statsFont.setPointSize(qMax(9, editorFont.pointSize() - 1));
|
||||||
|
m_statsLabel->setFont(statsFont);
|
||||||
|
|
||||||
|
m_statsLabel->setStyleSheet(QString(
|
||||||
|
"color: %1; padding: 4px 6px; background-color: %2; border-radius: 3px;")
|
||||||
|
.arg(Utils::creatorColor(Utils::Theme::TextColorNormal).name())
|
||||||
|
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorHover).name()));
|
||||||
|
|
||||||
|
updateButtonStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidget::updateButtonStyles()
|
||||||
|
{
|
||||||
|
if (!m_applyButton || !m_declineButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int baseFontSize = m_sourceEditor ? qMax(9, m_sourceEditor->font().pointSize() - 2) : 10;
|
||||||
|
|
||||||
|
auto createStyle = [&](const QColor &color, bool bold) {
|
||||||
|
return QString(
|
||||||
|
"QPushButton {"
|
||||||
|
" background-color: %1; color: %2; border: 1px solid %3;"
|
||||||
|
" border-radius: 3px; padding: 2px 8px; font-size: %4pt;%5"
|
||||||
|
"}"
|
||||||
|
"QPushButton:hover { background-color: %6; border: 1px solid %2; }"
|
||||||
|
"QPushButton:pressed { background-color: %7; }")
|
||||||
|
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorNormal).name())
|
||||||
|
.arg(color.name())
|
||||||
|
.arg(Utils::creatorColor(Utils::Theme::SplitterColor).name())
|
||||||
|
.arg(baseFontSize)
|
||||||
|
.arg(bold ? QLatin1StringView(" font-weight: bold;") : QLatin1StringView(""))
|
||||||
|
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorHover).name())
|
||||||
|
.arg(Utils::creatorColor(Utils::Theme::BackgroundColorSelected).name());
|
||||||
|
};
|
||||||
|
|
||||||
|
m_applyButton->setStyleSheet(createStyle(Utils::creatorColor(Utils::Theme::TextColorNormal), true));
|
||||||
|
m_declineButton->setStyleSheet(createStyle(Utils::creatorColor(Utils::Theme::TextColorError), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
141
widgets/RefactorWidget.hpp
Normal file
141
widgets/RefactorWidget.hpp
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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 setRange(const Utils::Text::Range &range);
|
||||||
|
void setEditorWidth(int width);
|
||||||
|
|
||||||
|
QString getRefactoredText() const;
|
||||||
|
|
||||||
|
void setApplyCallback(std::function<void(const QString &)> callback);
|
||||||
|
void setDeclineCallback(std::function<void()> callback);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void applied();
|
||||||
|
void declined();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
bool event(QEvent *event) override;
|
||||||
|
void closeEvent(QCloseEvent *event) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void syncLeftScroll(int value);
|
||||||
|
void syncRightScroll(int value);
|
||||||
|
void syncLeftHorizontalScroll(int value);
|
||||||
|
void syncRightHorizontalScroll(int value);
|
||||||
|
void onRightEditorTextChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
TextEditor::TextEditorWidget *m_sourceEditor;
|
||||||
|
TextEditor::TextEditorWidget *m_leftEditor;
|
||||||
|
TextEditor::TextEditorWidget *m_rightEditor;
|
||||||
|
QSharedPointer<TextEditor::TextDocument> m_leftDocument;
|
||||||
|
QSharedPointer<TextEditor::TextDocument> m_rightDocument;
|
||||||
|
QWidget *m_leftContainer;
|
||||||
|
QSplitter *m_splitter;
|
||||||
|
QLabel *m_statsLabel;
|
||||||
|
QPushButton *m_applyButton;
|
||||||
|
QPushButton *m_declineButton;
|
||||||
|
|
||||||
|
QString m_originalText;
|
||||||
|
QString m_refactoredText;
|
||||||
|
QString m_applyText;
|
||||||
|
QString m_contextBefore;
|
||||||
|
QString m_contextAfter;
|
||||||
|
Utils::Text::Range m_range;
|
||||||
|
int m_editorWidth;
|
||||||
|
bool m_syncingScroll;
|
||||||
|
bool m_isClosing;
|
||||||
|
int m_linesAdded;
|
||||||
|
int m_linesRemoved;
|
||||||
|
|
||||||
|
QList<Utils::Diff> m_cachedDiffList;
|
||||||
|
|
||||||
|
std::function<void(const QString &)> m_applyCallback;
|
||||||
|
std::function<void()> m_declineCallback;
|
||||||
|
|
||||||
|
void setupUi();
|
||||||
|
void applyRefactoring();
|
||||||
|
void declineRefactoring();
|
||||||
|
void updateSizeToContent();
|
||||||
|
void highlightDifferences();
|
||||||
|
void dimContextLines(const QString &contextBefore, const QString &contextAfter);
|
||||||
|
void calculateStats();
|
||||||
|
void updateStatsLabel();
|
||||||
|
void applySyntaxHighlighting();
|
||||||
|
void addLineMarkers();
|
||||||
|
void applyEditorSettings();
|
||||||
|
void updateButtonStyles();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
163
widgets/RefactorWidgetHandler.cpp
Normal file
163
widgets/RefactorWidgetHandler.cpp
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* 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 "ContextExtractor.hpp"
|
||||||
|
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QTextBlock>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
RefactorWidgetHandler::RefactorWidgetHandler(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
RefactorWidgetHandler::~RefactorWidgetHandler()
|
||||||
|
{
|
||||||
|
hideRefactorWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidgetHandler::showRefactorWidget(
|
||||||
|
TextEditor::TextEditorWidget *editor,
|
||||||
|
const QString &originalText,
|
||||||
|
const QString &refactoredText,
|
||||||
|
const Utils::Text::Range &range)
|
||||||
|
{
|
||||||
|
QString contextBefore = ContextExtractor::extractBefore(editor, range, 3);
|
||||||
|
QString contextAfter = ContextExtractor::extractAfter(editor, range, 3);
|
||||||
|
showRefactorWidget(editor, originalText, refactoredText, range, contextBefore, contextAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidgetHandler::showRefactorWidget(
|
||||||
|
TextEditor::TextEditorWidget *editor,
|
||||||
|
const QString &originalText,
|
||||||
|
const QString &refactoredText,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
const QString &contextBefore,
|
||||||
|
const QString &contextAfter)
|
||||||
|
{
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideRefactorWidget();
|
||||||
|
|
||||||
|
m_editor = editor;
|
||||||
|
m_refactorWidget = new RefactorWidget(editor);
|
||||||
|
|
||||||
|
m_refactorWidget->setDiffContent(originalText, refactoredText, contextBefore, contextAfter);
|
||||||
|
m_refactorWidget->setApplyText(refactoredText);
|
||||||
|
m_refactorWidget->setRange(range);
|
||||||
|
m_refactorWidget->setEditorWidth(getEditorWidth());
|
||||||
|
|
||||||
|
if (m_applyCallback) {
|
||||||
|
m_refactorWidget->setApplyCallback(m_applyCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_declineCallback) {
|
||||||
|
m_refactorWidget->setDeclineCallback(m_declineCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWidgetPosition();
|
||||||
|
m_refactorWidget->show();
|
||||||
|
m_refactorWidget->raise();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidgetHandler::hideRefactorWidget()
|
||||||
|
{
|
||||||
|
if (!m_refactorWidget.isNull()) {
|
||||||
|
m_refactorWidget->close();
|
||||||
|
m_refactorWidget = nullptr;
|
||||||
|
}
|
||||||
|
m_editor = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidgetHandler::setApplyCallback(std::function<void(const QString &)> callback)
|
||||||
|
{
|
||||||
|
m_applyCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidgetHandler::setDeclineCallback(std::function<void()> callback)
|
||||||
|
{
|
||||||
|
m_declineCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidgetHandler::setTextToApply(const QString &text)
|
||||||
|
{
|
||||||
|
if (!m_refactorWidget.isNull()) {
|
||||||
|
m_refactorWidget->setApplyText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefactorWidgetHandler::updateWidgetPosition()
|
||||||
|
{
|
||||||
|
if (m_refactorWidget.isNull() || m_editor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint position = calculateWidgetPosition();
|
||||||
|
m_refactorWidget->move(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint RefactorWidgetHandler::calculateWidgetPosition()
|
||||||
|
{
|
||||||
|
if (m_editor.isNull()) {
|
||||||
|
return QPoint(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor cursor = m_editor->textCursor();
|
||||||
|
QRect cursorRect = m_editor->cursorRect(cursor);
|
||||||
|
QPoint globalPos = m_editor->mapToGlobal(cursorRect.bottomLeft());
|
||||||
|
globalPos.setY(globalPos.y() + 10);
|
||||||
|
|
||||||
|
if (m_refactorWidget) {
|
||||||
|
QRect widgetRect(globalPos, m_refactorWidget->size());
|
||||||
|
QRect screenRect = m_editor->screen()->availableGeometry();
|
||||||
|
|
||||||
|
if (widgetRect.right() > screenRect.right()) {
|
||||||
|
globalPos.setX(screenRect.right() - m_refactorWidget->width() - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widgetRect.bottom() > screenRect.bottom()) {
|
||||||
|
globalPos.setY(m_editor->mapToGlobal(cursorRect.topLeft()).y()
|
||||||
|
- m_refactorWidget->height() - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalPos.x() < screenRect.left()) {
|
||||||
|
globalPos.setX(screenRect.left() + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalPos.y() < screenRect.top()) {
|
||||||
|
globalPos.setY(screenRect.top() + 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
int RefactorWidgetHandler::getEditorWidth() const
|
||||||
|
{
|
||||||
|
return m_editor.isNull() ? 800 : m_editor->viewport()->width();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
75
widgets/RefactorWidgetHandler.hpp
Normal file
75
widgets/RefactorWidgetHandler.hpp
Normal 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 showRefactorWidget(
|
||||||
|
TextEditor::TextEditorWidget *editor,
|
||||||
|
const QString &originalText,
|
||||||
|
const QString &refactoredText,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
const QString &contextBefore,
|
||||||
|
const QString &contextAfter);
|
||||||
|
|
||||||
|
void hideRefactorWidget();
|
||||||
|
|
||||||
|
bool isWidgetVisible() const { return !m_refactorWidget.isNull(); }
|
||||||
|
|
||||||
|
void setApplyCallback(std::function<void(const QString &)> callback);
|
||||||
|
void setDeclineCallback(std::function<void()> callback);
|
||||||
|
void setTextToApply(const QString &text);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<TextEditor::TextEditorWidget> m_editor;
|
||||||
|
QPointer<RefactorWidget> m_refactorWidget;
|
||||||
|
|
||||||
|
std::function<void(const QString &)> m_applyCallback;
|
||||||
|
std::function<void()> m_declineCallback;
|
||||||
|
|
||||||
|
void updateWidgetPosition();
|
||||||
|
QPoint calculateWidgetPosition();
|
||||||
|
int getEditorWidth() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
Reference in New Issue
Block a user