diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dddaad..0c1810f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ add_qtc_plugin(QodeAssist widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp + QuickRefactorHandler.hpp QuickRefactorHandler.cpp ) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index 6d5f3ed..9e5ae5a 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -24,8 +24,10 @@ #include "QodeAssistClient.hpp" +#include #include +#include #include #include @@ -35,6 +37,7 @@ #include "settings/GeneralSettings.hpp" #include "settings/ProjectSettings.hpp" #include +#include using namespace LanguageServerProtocol; using namespace TextEditor; @@ -170,6 +173,27 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor) sendMessage(request); } +void QodeAssistClient::requestQuickRefactor( + TextEditor::TextEditorWidget *editor, const QString &instructions) +{ + auto project = ProjectManager::projectForFile(editor->textDocument()->filePath()); + + if (!isEnabled(project)) + return; + + if (!m_refactorHandler) { + m_refactorHandler = new QuickRefactorHandler(this); + connect( + m_refactorHandler, + &QuickRefactorHandler::refactoringCompleted, + this, + &QodeAssistClient::handleRefactoringResult); + } + + m_progressHandler.showProgress(editor); + m_refactorHandler->sendRefactorRequest(editor, instructions); +} + void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor) { cancelRunningRequest(editor); @@ -301,4 +325,32 @@ void QodeAssistClient::cleanupConnections() m_scheduledRequests.clear(); } +void QodeAssistClient::handleRefactoringResult(const RefactorResult &result) +{ + m_progressHandler.hideProgress(); + if (!result.success) { + LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage)); + return; + } + + auto editor = BaseTextEditor::currentTextEditor(); + if (!editor) { + LOG_MESSAGE("Refactoring failed: No active editor found"); + return; + } + + auto editorWidget = editor->editorWidget(); + + QTextCursor cursor = editorWidget->textCursor(); + cursor.beginEditBlock(); + + int startPos = result.insertRange.begin.toPositionInDocument(editorWidget->document()); + int endPos = result.insertRange.end.toPositionInDocument(editorWidget->document()); + + cursor.setPosition(startPos); + cursor.setPosition(endPos, QTextCursor::KeepAnchor); + + cursor.insertText(result.newText); + cursor.endEditBlock(); +} } // namespace QodeAssist diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index 0bf8a39..bde3523 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -26,6 +26,7 @@ #include "LLMClientInterface.hpp" #include "LSPCompletion.hpp" +#include "QuickRefactorHandler.hpp" #include "widgets/CompletionProgressHandler.hpp" #include "widgets/EditorChatButtonHandler.hpp" #include @@ -44,6 +45,8 @@ public: bool canOpenProject(ProjectExplorer::Project *project) override; void requestCompletions(TextEditor::TextEditorWidget *editor); + void requestQuickRefactor( + TextEditor::TextEditorWidget *editor, const QString &instructions = QString()); private: void scheduleRequest(TextEditor::TextEditorWidget *editor); @@ -54,6 +57,7 @@ private: void setupConnections(); void cleanupConnections(); + void handleRefactoringResult(const RefactorResult &result); QHash m_runningRequests; QHash m_scheduledRequests; @@ -64,6 +68,7 @@ private: int m_recentCharCount; CompletionProgressHandler m_progressHandler; EditorChatButtonHandler m_chatButtonHandler; + QuickRefactorHandler *m_refactorHandler{nullptr}; }; } // namespace QodeAssist diff --git a/QuickRefactorHandler.cpp b/QuickRefactorHandler.cpp new file mode 100644 index 0000000..1263625 --- /dev/null +++ b/QuickRefactorHandler.cpp @@ -0,0 +1,293 @@ +/* + * 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 "QuickRefactorHandler.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QodeAssist { + +QuickRefactorHandler::QuickRefactorHandler(QObject *parent) + : QObject(parent) + , m_requestHandler(new LLMCore::RequestHandler(this)) + , m_currentEditor(nullptr) + , m_isRefactoringInProgress(false) + , m_contextManager(this) +{ + connect( + m_requestHandler, + &LLMCore::RequestHandler::completionReceived, + this, + &QuickRefactorHandler::handleLLMResponse); + + connect( + m_requestHandler, + &LLMCore::RequestHandler::requestFinished, + this, + [this](const QString &requestId, bool success, const QString &errorString) { + if (!success && requestId == m_lastRequestId) { + m_isRefactoringInProgress = false; + RefactorResult result; + result.success = false; + result.errorMessage = errorString; + emit refactoringCompleted(result); + } + }); +} + +QuickRefactorHandler::~QuickRefactorHandler() {} + +void QuickRefactorHandler::sendRefactorRequest( + TextEditor::TextEditorWidget *editor, const QString &instructions) +{ + if (m_isRefactoringInProgress) { + cancelRequest(); + } + + m_currentEditor = editor; + + Utils::Text::Range range; + if (editor->textCursor().hasSelection()) { + QTextCursor cursor = editor->textCursor(); + int startPos = cursor.selectionStart(); + int endPos = cursor.selectionEnd(); + + QTextBlock startBlock = editor->document()->findBlock(startPos); + int startLine = startBlock.blockNumber() + 1; + int startColumn = startPos - startBlock.position(); + + QTextBlock endBlock = editor->document()->findBlock(endPos); + int endLine = endBlock.blockNumber() + 1; + int endColumn = endPos - endBlock.position(); + + Utils::Text::Position startPosition; + startPosition.line = startLine; + startPosition.column = startColumn; + + Utils::Text::Position endPosition; + endPosition.line = endLine; + endPosition.column = endColumn; + + range = Utils::Text::Range(); + range.begin = startPosition; + range.end = endPosition; + } else { + QTextCursor cursor = editor->textCursor(); + int cursorPos = cursor.position(); + + QTextBlock block = editor->document()->findBlock(cursorPos); + int line = block.blockNumber() + 1; + int column = cursorPos - block.position(); + + Utils::Text::Position cursorPosition; + cursorPosition.line = line; + cursorPosition.column = column; + range = Utils::Text::Range(); + range.begin = cursorPosition; + range.end = cursorPosition; + } + + m_currentRange = range; + prepareAndSendRequest(editor, instructions, range); +} + +void QuickRefactorHandler::prepareAndSendRequest( + TextEditor::TextEditorWidget *editor, + const QString &instructions, + const Utils::Text::Range &range) +{ + auto &settings = Settings::generalSettings(); + + auto &providerRegistry = LLMCore::ProvidersManager::instance(); + auto &promptManager = LLMCore::PromptTemplateManager::instance(); + + const auto providerName = settings.caProvider(); + auto provider = providerRegistry.getProviderByName(providerName); + + if (!provider) { + LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); + RefactorResult result; + result.success = false; + result.errorMessage = QString("No provider found with name: %1").arg(providerName); + emit refactoringCompleted(result); + return; + } + + const auto templateName = settings.caTemplate(); + auto promptTemplate = promptManager.getChatTemplateByName(templateName); + + if (!promptTemplate) { + LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); + RefactorResult result; + result.success = false; + result.errorMessage = QString("No template found with name: %1").arg(templateName); + emit refactoringCompleted(result); + return; + } + + LLMCore::LLMConfig config; + config.requestType = LLMCore::RequestType::Chat; + config.provider = provider; + config.promptTemplate = promptTemplate; + config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint()); + config.providerRequest + = {{"model", settings.caModel()}, {"stream", Settings::chatAssistantSettings().stream()}}; + config.apiKey = provider->apiKey(); + + LLMCore::ContextData context = prepareContext(editor, range, instructions); + + provider + ->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat); + + QString requestId = QUuid::createUuid().toString(); + m_lastRequestId = requestId; + QJsonObject request{{"id", requestId}}; + + m_isRefactoringInProgress = true; + + m_requestHandler->sendLLMRequest(config, request); +} + +LLMCore::ContextData QuickRefactorHandler::prepareContext( + TextEditor::TextEditorWidget *editor, + const Utils::Text::Range &range, + const QString &instructions) +{ + LLMCore::ContextData context; + + auto textDocument = editor->textDocument(); + Context::DocumentReaderQtCreator documentReader; + auto documentInfo = documentReader.readDocument(textDocument->filePath().toUrlishString()); + + if (!documentInfo.document) { + LOG_MESSAGE("Error: Document is not available"); + return context; + } + + QTextCursor cursor = editor->textCursor(); + int cursorPos = cursor.position(); + + // TODO add selecting content before and after cursor/selection + QString fullContent = documentInfo.document->toPlainText(); + QString taggedContent = fullContent; + + if (cursor.hasSelection()) { + int selEnd = cursor.selectionEnd(); + int selStart = cursor.selectionStart(); + taggedContent + .insert(selEnd, selEnd == cursorPos ? "" : ""); + taggedContent.insert( + selStart, selStart == cursorPos ? "" : ""); + } else { + taggedContent.insert(cursorPos, ""); + } + + QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt(); + systemPrompt += "\n\nFile information:"; + systemPrompt += "\nLanguage: " + documentInfo.mimeType; + systemPrompt += "\nFile path: " + documentInfo.filePath; + + systemPrompt += "\n\nCode context with position markers:"; + systemPrompt += taggedContent; + + systemPrompt += "\n\nOutput format:"; + systemPrompt += "\n- Generate ONLY the code that should replace the current selection " + "between or be " + "inserted at cursor position"; + systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown " + "code block markers"; + systemPrompt += "\n- The output should be ready to insert directly into the editor"; + systemPrompt += "\n- Follow the existing code style and indentation patterns"; + + if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) { + systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath}); + } + + context.systemPrompt = systemPrompt; + + QVector messages; + messages.append( + {"user", + instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability." + : instructions}); + context.history = messages; + + return context; +} + +void QuickRefactorHandler::handleLLMResponse( + const QString &response, const QJsonObject &request, bool isComplete) +{ + if (request["id"].toString() != m_lastRequestId) { + return; + } + + if (isComplete) { + 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; + result.newText = cleanedResponse; + result.insertRange = m_currentRange; + result.success = true; + + LOG_MESSAGE("Refactoring completed successfully. New code to insert: "); + LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------"); + LOG_MESSAGE(cleanedResponse); + LOG_MESSAGE("----------- END REFACTORED CODE -----------"); + + emit refactoringCompleted(result); + } +} + +void QuickRefactorHandler::cancelRequest() +{ + if (m_isRefactoringInProgress) { + m_requestHandler->cancelRequest(m_lastRequestId); + m_isRefactoringInProgress = false; + + RefactorResult result; + result.success = false; + result.errorMessage = "Refactoring request was cancelled"; + emit refactoringCompleted(result); + } +} + +} // namespace QodeAssist diff --git a/QuickRefactorHandler.hpp b/QuickRefactorHandler.hpp new file mode 100644 index 0000000..886e5b8 --- /dev/null +++ b/QuickRefactorHandler.hpp @@ -0,0 +1,77 @@ +/* + * 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 + +namespace QodeAssist { + +struct RefactorResult +{ + QString newText; + Utils::Text::Range insertRange; + bool success; + QString errorMessage; +}; + +class QuickRefactorHandler : public QObject +{ + Q_OBJECT + +public: + explicit QuickRefactorHandler(QObject *parent = nullptr); + ~QuickRefactorHandler() override; + + void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions); + + void cancelRequest(); + +signals: + void refactoringCompleted(const QodeAssist::RefactorResult &result); + +private: + void prepareAndSendRequest( + TextEditor::TextEditorWidget *editor, + const QString &instructions, + const Utils::Text::Range &range); + + void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); + LLMCore::ContextData prepareContext( + TextEditor::TextEditorWidget *editor, + const Utils::Text::Range &range, + const QString &instructions); + + LLMCore::RequestHandler *m_requestHandler; + TextEditor::TextEditorWidget *m_currentEditor; + Utils::Text::Range m_currentRange; + bool m_isRefactoringInProgress; + QString m_lastRequestId; + Context::ContextManager m_contextManager; +}; + +} // namespace QodeAssist diff --git a/qodeassist.cpp b/qodeassist.cpp index 7e61399..8b6da81 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -43,6 +43,7 @@ #include "ConfigurationManager.hpp" #include "QodeAssistClient.hpp" +#include "UpdateStatusWidget.hpp" #include "Version.hpp" #include "chat/ChatOutputPane.h" #include "chat/NavigationPanel.hpp" @@ -50,13 +51,17 @@ #include "llmcore/PromptProviderFim.hpp" #include "llmcore/ProvidersManager.hpp" #include "logger/RequestPerformanceLogger.hpp" +#include "providers/Providers.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProjectSettingsPanel.hpp" #include "settings/SettingsConstants.hpp" - -#include "UpdateStatusWidget.hpp" -#include "providers/Providers.hpp" #include "templates/Templates.hpp" +#include +#include +#include +#include +#include +#include using namespace Utils; using namespace Core; @@ -134,6 +139,41 @@ public: if (Settings::generalSettings().enableCheckUpdate()) { QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates); } + + ActionBuilder quickRefactorAction(this, "QodeAssist.QuickRefactor"); + const QKeySequence quickRefactorShortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_R); + quickRefactorAction.setDefaultKeySequence(quickRefactorShortcut); + quickRefactorAction.setToolTip(Tr::tr("Refactor code using QodeAssist")); + quickRefactorAction.setText(Tr::tr("Quick Refactor with QodeAssist")); + quickRefactorAction.setIcon(QCODEASSIST_ICON.icon()); + quickRefactorAction.addOnTriggered(this, [this] { + if (auto editor = TextEditor::TextEditorWidget::currentTextEditorWidget()) { + bool ok; + if (m_qodeAssistClient && m_qodeAssistClient->reachable()) { + QString instructions = QInputDialog::getText( + Core::ICore::dialogParent(), + Tr::tr("Quick Refactor"), + Tr::tr("Enter refactoring instructions:"), + QLineEdit::Normal, + QString(), + &ok); + if (ok) + m_qodeAssistClient->requestQuickRefactor(editor, instructions); + } else { + qWarning() << "The QodeAssist is not ready. Please check your connection and " + "settings."; + } + } + }); + + Core::ActionContainer *editorContextMenu = Core::ActionManager::actionContainer( + TextEditor::Constants::M_STANDARDCONTEXTMENU); + if (editorContextMenu) { + editorContextMenu->addSeparator(Core::Context(TextEditor::Constants::C_TEXTEDITOR)); + editorContextMenu + ->addAction(quickRefactorAction.command(), Core::Constants::G_DEFAULT_THREE); + editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE); + } } void extensionsInitialized() final {} diff --git a/settings/CodeCompletionSettings.cpp b/settings/CodeCompletionSettings.cpp index 1f404c3..5ff5ac5 100644 --- a/settings/CodeCompletionSettings.cpp +++ b/settings/CodeCompletionSettings.cpp @@ -218,6 +218,18 @@ CodeCompletionSettings::CodeCompletionSettings() maxChangesCacheSize.setRange(2, 1000); maxChangesCacheSize.setDefaultValue(10); + // Quick refactor command settings + useOpenFilesInQuickRefactor.setSettingsKey(Constants::CC_USE_OPEN_FILES_IN_QUICK_REFACTOR); + useOpenFilesInQuickRefactor.setLabelText( + Tr::tr("Include context from open files in quick refactor")); + useOpenFilesInQuickRefactor.setDefaultValue(false); + quickRefactorSystemPrompt.setSettingsKey(Constants::CC_QUICK_REFACTOR_SYSTEM_PROMPT); + quickRefactorSystemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay); + quickRefactorSystemPrompt.setDefaultValue( + "You are an expert C++, Qt, and QML code completion assistant. Your task is to provide" + "precise and contextually appropriate code completions to insert depending on user " + "instructions.\n\n"); + // Ollama Settings ollamaLivetime.setSettingsKey(Constants::CC_OLLAMA_LIVETIME); ollamaLivetime.setToolTip( @@ -303,6 +315,10 @@ CodeCompletionSettings::CodeCompletionSettings() Space{8}, Group{title(Tr::tr("Context Settings")), contextItem}, Space{8}, + Group{ + title(Tr::tr("Quick Refactor Settings")), + Column{useOpenFilesInQuickRefactor, quickRefactorSystemPrompt}}, + Space{8}, Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}}, Stretch{1}}; }); @@ -371,6 +387,8 @@ void CodeCompletionSettings::resetSettingsToDefaults() resetAspect(customLanguages); resetAspect(showProgressWidget); resetAspect(useOpenFilesContext); + resetAspect(useOpenFilesInQuickRefactor); + resetAspect(quickRefactorSystemPrompt); } } diff --git a/settings/CodeCompletionSettings.hpp b/settings/CodeCompletionSettings.hpp index 3a26fbb..b81dcdd 100644 --- a/settings/CodeCompletionSettings.hpp +++ b/settings/CodeCompletionSettings.hpp @@ -77,6 +77,10 @@ public: Utils::BoolAspect useProjectChangesCache{this}; Utils::IntegerAspect maxChangesCacheSize{this}; + // Quick refactor command settings + Utils::BoolAspect useOpenFilesInQuickRefactor{this}; + Utils::StringAspect quickRefactorSystemPrompt{this}; + // Ollama Settings Utils::StringAspect ollamaLivetime{this}; Utils::IntegerAspect contextWindow{this}; diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index dd2e590..7dc5fab 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -120,6 +120,10 @@ const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize"; const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt"; const char CA_SYSTEM_PROMPT[] = "QodeAssist.chatSystemPrompt"; +// quick refactor command settings +const char CC_QUICK_REFACTOR_SYSTEM_PROMPT[] = "QodeAssist.ccQuickRefactorSystemPrompt"; +const char CC_USE_OPEN_FILES_IN_QUICK_REFACTOR[] = "QodeAssist.ccUseOpenFilesInQuickRefactor"; + // preset prompt settings const char CC_TEMPERATURE[] = "QodeAssist.ccTemperature"; const char CC_MAX_TOKENS[] = "QodeAssist.ccMaxTokens";