diff --git a/CMakeLists.txt b/CMakeLists.txt index dc5bc0a..4537044 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,8 @@ add_qtc_plugin(QodeAssist QodeAssist.qrc LSPCompletion.hpp LLMSuggestion.hpp LLMSuggestion.cpp + RefactorSuggestion.hpp RefactorSuggestion.cpp + RefactorSuggestionHoverHandler.hpp RefactorSuggestionHoverHandler.cpp QodeAssistClient.hpp QodeAssistClient.cpp chat/ChatOutputPane.h chat/ChatOutputPane.cpp chat/NavigationPanel.hpp chat/NavigationPanel.cpp @@ -109,10 +111,13 @@ add_qtc_plugin(QodeAssist CodeHandler.hpp CodeHandler.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp + widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp + widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp + QuickRefactorHandler.hpp QuickRefactorHandler.cpp tools/ToolsFactory.hpp tools/ToolsFactory.cpp tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index 185fbc8..053fa63 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -158,7 +158,12 @@ void ClientInterface::sendMessage( config.apiKey = provider->apiKey(); config.provider->prepareRequest( - config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat, isToolsEnabled); + config.providerRequest, + promptTemplate, + context, + LLMCore::RequestType::Chat, + isToolsEnabled, + Settings::chatAssistantSettings().enableThinkingMode()); QString requestId = QUuid::createUuid().toString(); QJsonObject request{{"id", requestId}}; diff --git a/ConfigurationManager.cpp b/ConfigurationManager.cpp index c665a1e..23ab07d 100644 --- a/ConfigurationManager.cpp +++ b/ConfigurationManager.cpp @@ -51,6 +51,8 @@ void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect & m_generalSettings.ccTemplateDescription.setValue(templ->description()); } else if (&templateAspect == &m_generalSettings.caTemplate) { m_generalSettings.caTemplateDescription.setValue(templ->description()); + } else if (&templateAspect == &m_generalSettings.qrTemplate) { + m_generalSettings.qrTemplateDescription.setValue(templ->description()); } } @@ -58,6 +60,7 @@ void ConfigurationManager::updateAllTemplateDescriptions() { updateTemplateDescription(m_generalSettings.ccTemplate); updateTemplateDescription(m_generalSettings.caTemplate); + updateTemplateDescription(m_generalSettings.qrTemplate); } void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect) @@ -94,12 +97,16 @@ void ConfigurationManager::setupConnections() connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider); connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider); + connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider); connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel); connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel); + connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel); connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate); connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate); + connect(&m_generalSettings.qrSelectTemplate, &Button::clicked, this, &Config::selectTemplate); connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl); + connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl); connect( &m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider); @@ -115,6 +122,10 @@ void ConfigurationManager::setupConnections() connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() { updateTemplateDescription(m_generalSettings.caTemplate); }); + + connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() { + updateTemplateDescription(m_generalSettings.qrTemplate); + }); } void ConfigurationManager::selectProvider() @@ -129,6 +140,8 @@ void ConfigurationManager::selectProvider() ? m_generalSettings.ccProvider : settingsButton == &m_generalSettings.ccPreset1SelectProvider ? m_generalSettings.ccPreset1Provider + : settingsButton == &m_generalSettings.qrSelectProvider + ? m_generalSettings.qrProvider : m_generalSettings.caProvider; QTimer::singleShot(0, this, [this, providersList, &targetSettings] { @@ -145,17 +158,21 @@ void ConfigurationManager::selectModel() const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel); const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel); + const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel); const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue() : isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue() + : isQuickRefactor ? m_generalSettings.qrProvider.volatileValue() : m_generalSettings.caProvider.volatileValue(); const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue() : isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue() + : isQuickRefactor ? m_generalSettings.qrUrl.volatileValue() : m_generalSettings.caUrl.volatileValue(); auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel : isPreset1 ? m_generalSettings.ccPreset1Model + : isQuickRefactor ? m_generalSettings.qrModel : m_generalSettings.caModel; if (auto provider = m_providersManager.getProviderByName(providerName)) { @@ -186,8 +203,10 @@ void ConfigurationManager::selectTemplate() const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate); const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate); + const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate); const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue() : isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue() + : isQuickRefactor ? m_generalSettings.qrProvider.volatileValue() : m_generalSettings.caProvider.volatileValue(); auto providerID = m_providersManager.getProviderByName(providerName)->providerID(); @@ -197,6 +216,7 @@ void ConfigurationManager::selectTemplate() auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate : isPreset1 ? m_generalSettings.ccPreset1Template + : isQuickRefactor ? m_generalSettings.qrTemplate : m_generalSettings.caTemplate; QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() { @@ -221,6 +241,8 @@ void ConfigurationManager::selectUrl() auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl : settingsButton == &m_generalSettings.ccPreset1SetUrl ? m_generalSettings.ccPreset1Url + : settingsButton == &m_generalSettings.qrSetUrl + ? m_generalSettings.qrUrl : m_generalSettings.caUrl; QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() { diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index e1476be..6eb23fa 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -86,7 +86,22 @@ void LLMClientInterface::handleRequestFailed(const QString &requestId, const QSt return; LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error)); + + // Send LSP error response to client + const RequestContext &ctx = it.value(); + QJsonObject response; + response["jsonrpc"] = "2.0"; + response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"]; + + QJsonObject errorObject; + errorObject["code"] = -32603; // Internal error code + errorObject["message"] = error; + response["error"] = errorObject; + + emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); + m_activeRequests.erase(it); + m_performanceLogger.endTimeMeasurement(requestId); } void LLMClientInterface::sendData(const QByteArray &data) @@ -110,7 +125,8 @@ void LLMClientInterface::sendData(const QByteArray &data) QString requestId = request["id"].toString(); m_performanceLogger.startTimeMeasurement(requestId); handleCompletion(request); - } else if (method == "$/cancelRequest") { + } else if (method == "cancelRequest") { + qDebug() << "Cancelling request"; handleCancelRequest(); } else if (method == "exit") { // TODO make exit handler @@ -194,12 +210,32 @@ void LLMClientInterface::handleExit(const QJsonObject &request) emit finished(); } +void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage) +{ + QJsonObject response; + response["jsonrpc"] = "2.0"; + response[LanguageServerProtocol::idKey] = request["id"]; + + QJsonObject errorObject; + errorObject["code"] = -32603; // Internal error code + errorObject["message"] = errorMessage; + response["error"] = errorObject; + + emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); + + // End performance measurement if it was started + QString requestId = request["id"].toString(); + m_performanceLogger.endTimeMeasurement(requestId); +} + void LLMClientInterface::handleCompletion(const QJsonObject &request) { auto filePath = Context::extractFilePathFromRequest(request); auto documentInfo = m_documentReader.readDocument(filePath); if (!documentInfo.document) { - LOG_MESSAGE("Error: Document is not available for" + filePath); + QString error = QString("Document is not available: %1").arg(filePath); + LOG_MESSAGE("Error: " + error); + sendErrorResponse(request, error); return; } @@ -217,7 +253,9 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) const auto provider = m_providerRegistry.getProviderByName(providerName); if (!provider) { - LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); + QString error = QString("No provider found with name: %1").arg(providerName); + LOG_MESSAGE(error); + sendErrorResponse(request, error); return; } @@ -227,7 +265,9 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) auto promptTemplate = m_promptProvider->getTemplateByName(templateName); if (!promptTemplate) { - LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); + QString error = QString("No template found with name: %1").arg(templateName); + LOG_MESSAGE(error); + sendErrorResponse(request, error); return; } @@ -309,12 +349,15 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) promptTemplate, updatedContext, LLMCore::RequestType::CodeCompletion, + false, false); auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type()); if (!errors.isEmpty()) { - LOG_MESSAGE("Validate errors for fim request:"); + QString error = QString("Request validation failed: %1").arg(errors.join("; ")); + LOG_MESSAGE("Validate errors for request:"); LOG_MESSAGES(errors); + sendErrorResponse(request, error); return; } diff --git a/LLMClientInterface.hpp b/LLMClientInterface.hpp index 3f25e29..f1b7b24 100644 --- a/LLMClientInterface.hpp +++ b/LLMClientInterface.hpp @@ -77,6 +77,7 @@ private: void handleInitialized(const QJsonObject &request); void handleExit(const QJsonObject &request); void handleCancelRequest(); + void sendErrorResponse(const QJsonObject &request, const QString &errorMessage); struct RequestContext { diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index bdbae3d..2205700 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -24,7 +24,9 @@ #include "QodeAssistClient.hpp" +#include #include +#include #include #include @@ -33,6 +35,8 @@ #include "LLMClientInterface.hpp" #include "LLMSuggestion.hpp" +#include "RefactorSuggestion.hpp" +#include "RefactorSuggestionHoverHandler.hpp" #include "settings/CodeCompletionSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProjectSettings.hpp" @@ -61,11 +65,15 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface) setupConnections(); m_typingTimer.start(); + + // Create hover handler for refactoring suggestions + m_refactorHoverHandler = new RefactorSuggestionHoverHandler(); } QodeAssistClient::~QodeAssistClient() { cleanupConnections(); + delete m_refactorHoverHandler; } void QodeAssistClient::openDocument(TextEditor::TextDocument *document) @@ -75,6 +83,14 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document) return; Client::openDocument(document); + + // Register hover handler for this document + auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document); + for (auto *editor : editors) { + if (auto *widget = editor->editorWidget()) { + widget->addHoverHandler(m_refactorHoverHandler); + } + } connect( document, &TextDocument::contentsChangedWithPosition, @@ -175,6 +191,7 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor) } request.setResponseCallback([this, editor = QPointer(editor)]( const GetCompletionRequest::Response &response) { + qDebug() << "setResponseCallback"; QTC_ASSERT(editor, return); handleCompletions(response, editor); }); @@ -243,8 +260,17 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor) void QodeAssistClient::handleCompletions( const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor) { - if (response.error()) + qDebug() << "hideProgress"; + m_progressHandler.hideProgress(); + + if (response.error()) { log(*response.error()); + + QString errorMessage = tr("Code completion failed: %1") + .arg(response.error()->message()); + m_errorHandler.showError(editor, errorMessage); + return; + } int requestPosition = -1; if (const auto requestParams = m_runningRequests.take(editor).params()) @@ -288,9 +314,11 @@ void QodeAssistClient::handleCompletions( Text::Position pos{toTextPos(c.position())}; return TextSuggestion::Data{range, pos, c.text()}; }); - m_progressHandler.hideProgress(); - if (completions.isEmpty()) + + if (completions.isEmpty()) { + LOG_MESSAGE("No valid completions received"); return; + } editor->insertSuggestion(std::make_unique(suggestions, editor->document())); } } @@ -344,30 +372,85 @@ void QodeAssistClient::cleanupConnections() void QodeAssistClient::handleRefactoringResult(const RefactorResult &result) { + m_progressHandler.hideProgress(); + if (!result.success) { + // Show error to user + QString errorMessage = result.errorMessage.isEmpty() + ? tr("Quick refactor failed") + : tr("Quick refactor failed: %1").arg(result.errorMessage); + + if (result.editor) { + m_errorHandler.showError(result.editor, errorMessage); + } + LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage)); return; } - auto editor = BaseTextEditor::currentTextEditor(); - if (!editor) { - LOG_MESSAGE("Refactoring failed: No active editor found"); + if (!result.editor) { + LOG_MESSAGE("Refactoring result has no editor"); return; } - auto editorWidget = editor->editorWidget(); + TextEditorWidget *editorWidget = result.editor; - QTextCursor cursor = editorWidget->textCursor(); - cursor.beginEditBlock(); + auto toTextPos = [](const Utils::Text::Position &pos) { + return Utils::Text::Position{pos.line, pos.column}; + }; - int startPos = result.insertRange.begin.toPositionInDocument(editorWidget->document()); - int endPos = result.insertRange.end.toPositionInDocument(editorWidget->document()); + Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)}; + Utils::Text::Position pos = toTextPos(result.insertRange.begin); - cursor.setPosition(startPos); - cursor.setPosition(endPos, QTextCursor::KeepAnchor); + int startPos = range.begin.toPositionInDocument(editorWidget->document()); + int endPos = range.end.toPositionInDocument(editorWidget->document()); + + if (startPos != endPos) { + QTextCursor startCursor(editorWidget->document()); + startCursor.setPosition(startPos); + if (startCursor.positionInBlock() > 0) { + startCursor.movePosition(QTextCursor::StartOfBlock); + } + + QTextCursor endCursor(editorWidget->document()); + endCursor.setPosition(endPos); + if (endCursor.positionInBlock() > 0) { + endCursor.movePosition(QTextCursor::EndOfBlock); + if (!endCursor.atEnd()) { + endCursor.movePosition(QTextCursor::NextCharacter); + } + } + + Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument( + editorWidget->document(), startCursor.position()); + Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument( + editorWidget->document(), endCursor.position()); + + range = Utils::Text::Range(expandedBegin, expandedEnd); + } - cursor.insertText(result.newText); - cursor.endEditBlock(); - m_progressHandler.hideProgress(); + TextEditor::TextSuggestion::Data suggestionData{ + Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)}, + pos, + result.newText + }; + editorWidget->insertSuggestion( + std::make_unique(suggestionData, editorWidget->document())); + + m_refactorHoverHandler->setSuggestionRange(range); + + m_refactorHoverHandler->setApplyCallback([this, editorWidget]() { + QKeyEvent tabEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier); + QApplication::sendEvent(editorWidget, &tabEvent); + m_refactorHoverHandler->clearSuggestionRange(); + }); + + m_refactorHoverHandler->setDismissCallback([this, editorWidget]() { + editorWidget->clearSuggestion(); + m_refactorHoverHandler->clearSuggestionRange(); + }); + + LOG_MESSAGE("Displaying refactoring suggestion with hover handler"); } + } // namespace QodeAssist diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index e812d22..b01f8eb 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -29,7 +29,9 @@ #include "LLMClientInterface.hpp" #include "LSPCompletion.hpp" #include "QuickRefactorHandler.hpp" +#include "RefactorSuggestionHoverHandler.hpp" #include "widgets/CompletionProgressHandler.hpp" +#include "widgets/CompletionErrorHandler.hpp" #include "widgets/EditorChatButtonHandler.hpp" #include #include @@ -70,8 +72,10 @@ private: QElapsedTimer m_typingTimer; int m_recentCharCount; CompletionProgressHandler m_progressHandler; + CompletionErrorHandler m_errorHandler; EditorChatButtonHandler m_chatButtonHandler; QuickRefactorHandler *m_refactorHandler{nullptr}; + RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; LLMClientInterface *m_llmClient; }; diff --git a/QuickRefactorHandler.cpp b/QuickRefactorHandler.cpp index c416d92..3b95f4f 100644 --- a/QuickRefactorHandler.cpp +++ b/QuickRefactorHandler.cpp @@ -33,6 +33,7 @@ #include #include #include +#include namespace QodeAssist { @@ -110,26 +111,30 @@ void QuickRefactorHandler::prepareAndSendRequest( auto &providerRegistry = LLMCore::ProvidersManager::instance(); auto &promptManager = LLMCore::PromptTemplateManager::instance(); - const auto providerName = settings.caProvider(); + const auto providerName = settings.qrProvider(); auto provider = providerRegistry.getProviderByName(providerName); if (!provider) { - LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); + QString error = QString("No provider found with name: %1").arg(providerName); + LOG_MESSAGE(error); RefactorResult result; result.success = false; - result.errorMessage = QString("No provider found with name: %1").arg(providerName); + result.errorMessage = error; + result.editor = editor; emit refactoringCompleted(result); return; } - const auto templateName = settings.caTemplate(); + const auto templateName = settings.qrTemplate(); auto promptTemplate = promptManager.getChatTemplateByName(templateName); if (!promptTemplate) { - LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); + QString error = QString("No template found with name: %1").arg(templateName); + LOG_MESSAGE(error); RefactorResult result; result.success = false; - result.errorMessage = QString("No template found with name: %1").arg(templateName); + result.errorMessage = error; + result.editor = editor; emit refactoringCompleted(result); return; } @@ -138,18 +143,34 @@ void QuickRefactorHandler::prepareAndSendRequest( config.requestType = LLMCore::RequestType::QuickRefactoring; config.provider = provider; config.promptTemplate = promptTemplate; - config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint()); - config.providerRequest = {{"model", settings.caModel()}, {"stream", true}}; + config.url = QString("%1%2").arg(settings.qrUrl(), provider->chatEndpoint()); config.apiKey = provider->apiKey(); + if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { + QString stream = QString{"streamGenerateContent?alt=sse"}; + config.url = QUrl(QString("%1/models/%2:%3") + .arg( + Settings::generalSettings().qrUrl(), + Settings::generalSettings().qrModel(), + stream)); + } else { + config.url + = QString("%1%2").arg(Settings::generalSettings().qrUrl(), provider->chatEndpoint()); + config.providerRequest + = {{"model", Settings::generalSettings().qrModel()}, {"stream", true}}; + } + LLMCore::ContextData context = prepareContext(editor, range, instructions); + bool enableTools = Settings::quickRefactorSettings().useTools(); + bool enableThinking = Settings::quickRefactorSettings().useThinking(); provider->prepareRequest( config.providerRequest, promptTemplate, context, LLMCore::RequestType::QuickRefactoring, - false); + enableTools, + enableThinking); QString requestId = QUuid::createUuid().toString(); m_lastRequestId = requestId; @@ -195,22 +216,75 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext( 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; + Context::DocumentContextReader + reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath); + + QString taggedContent; + bool readFullFile = Settings::quickRefactorSettings().readFullFile(); if (cursor.hasSelection()) { - int selEnd = cursor.selectionEnd(); int selStart = cursor.selectionStart(); - taggedContent - .insert(selEnd, selEnd == cursorPos ? "" : ""); - taggedContent.insert( - selStart, selStart == cursorPos ? "" : ""); + int selEnd = cursor.selectionEnd(); + + QTextBlock startBlock = documentInfo.document->findBlock(selStart); + int startLine = startBlock.blockNumber(); + int startColumn = selStart - startBlock.position(); + + QTextBlock endBlock = documentInfo.document->findBlock(selEnd); + int endLine = endBlock.blockNumber(); + int endColumn = selEnd - endBlock.position(); + + QString contextBefore; + if (readFullFile) { + contextBefore = reader.readWholeFileBefore(startLine, startColumn); + } else { + contextBefore = reader.getContextBefore( + startLine, startColumn, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1); + } + + QString selectedText = cursor.selectedText(); + selectedText.replace(QChar(0x2029), "\n"); + + QString contextAfter; + if (readFullFile) { + contextAfter = reader.readWholeFileAfter(endLine, endColumn); + } else { + contextAfter = reader.getContextAfter( + endLine, endColumn, Settings::quickRefactorSettings().readStringsAfterCursor() + 1); + } + + taggedContent = contextBefore; + if (selStart == cursorPos) { + taggedContent += "" + selectedText + ""; + } else { + taggedContent += "" + selectedText + ""; + } + taggedContent += contextAfter; } else { - taggedContent.insert(cursorPos, ""); + QTextBlock block = documentInfo.document->findBlock(cursorPos); + int line = block.blockNumber(); + int column = cursorPos - block.position(); + + QString contextBefore; + if (readFullFile) { + contextBefore = reader.readWholeFileBefore(line, column); + } else { + contextBefore = reader.getContextBefore( + line, column, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1); + } + + QString contextAfter; + if (readFullFile) { + contextAfter = reader.readWholeFileAfter(line, column); + } else { + contextAfter = reader.getContextAfter( + line, column, Settings::quickRefactorSettings().readStringsAfterCursor() + 1); + } + + taggedContent = contextBefore + "" + contextAfter; } - QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt(); + QString systemPrompt = Settings::quickRefactorSettings().systemPrompt(); auto project = LLMCore::RulesLoader::getActiveProject(); if (project) { @@ -263,6 +337,8 @@ void QuickRefactorHandler::handleLLMResponse( } if (isComplete) { + m_isRefactoringInProgress = false; + QString cleanedResponse = response.trimmed(); if (cleanedResponse.startsWith("```")) { int firstNewLine = cleanedResponse.indexOf('\n'); @@ -280,6 +356,7 @@ void QuickRefactorHandler::handleLLMResponse( result.newText = cleanedResponse; result.insertRange = m_currentRange; result.success = true; + result.editor = m_currentEditor; LOG_MESSAGE("Refactoring completed successfully. New code to insert: "); LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------"); @@ -316,6 +393,7 @@ void QuickRefactorHandler::cancelRequest() void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText) { if (requestId == m_lastRequestId) { + m_activeRequests.remove(requestId); QJsonObject request{{"id", requestId}}; handleLLMResponse(fullText, request, true); } @@ -324,10 +402,12 @@ void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QS void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error) { if (requestId == m_lastRequestId) { + m_activeRequests.remove(requestId); m_isRefactoringInProgress = false; RefactorResult result; result.success = false; result.errorMessage = error; + result.editor = m_currentEditor; emit refactoringCompleted(result); } } diff --git a/QuickRefactorHandler.hpp b/QuickRefactorHandler.hpp index ecb15a7..d42182f 100644 --- a/QuickRefactorHandler.hpp +++ b/QuickRefactorHandler.hpp @@ -38,6 +38,7 @@ struct RefactorResult Utils::Text::Range insertRange; bool success; QString errorMessage; + TextEditor::TextEditorWidget *editor{nullptr}; }; class QuickRefactorHandler : public QObject diff --git a/RefactorSuggestion.cpp b/RefactorSuggestion.cpp new file mode 100644 index 0000000..221c856 --- /dev/null +++ b/RefactorSuggestion.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2024-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 "RefactorSuggestion.hpp" +#include "LLMSuggestion.hpp" + +#include +#include +#include + +#include +#include + +namespace QodeAssist { + +namespace { +QString extractLeadingWhitespace(const QString &text) +{ + QString indent; + int firstLineEnd = text.indexOf('\n'); + QString firstLine = (firstLineEnd != -1) ? text.left(firstLineEnd) : text; + for (int i = 0; i < firstLine.length(); ++i) { + if (firstLine[i].isSpace()) { + indent += firstLine[i]; + } else { + break; + } + } + return indent; +} +} // anonymous namespace + +RefactorSuggestion::RefactorSuggestion(const Data &suggestion, QTextDocument *sourceDocument) + : TextEditor::TextSuggestion([&suggestion, sourceDocument]() { + Data expandedData = suggestion; + + int startPos = suggestion.range.begin.toPositionInDocument(sourceDocument); + int endPos = suggestion.range.end.toPositionInDocument(sourceDocument); + startPos = qBound(0, startPos, sourceDocument->characterCount()); + endPos = qBound(0, endPos, sourceDocument->characterCount()); + + if (startPos != endPos) { + QTextCursor startCursor(sourceDocument); + startCursor.setPosition(startPos); + int startPosInBlock = startCursor.positionInBlock(); + + if (startPosInBlock > 0) { + startCursor.movePosition(QTextCursor::StartOfBlock); + } + + QTextCursor endCursor(sourceDocument); + endCursor.setPosition(endPos); + int endPosInBlock = endCursor.positionInBlock(); + + if (endPosInBlock > 0) { + endCursor.movePosition(QTextCursor::EndOfBlock); + if (!endCursor.atEnd()) { + endCursor.movePosition(QTextCursor::NextCharacter); + } + } + + Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument( + sourceDocument, startCursor.position()); + Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument( + sourceDocument, endCursor.position()); + + expandedData.range = Utils::Text::Range(expandedBegin, expandedEnd); + } + + return expandedData; + }(), sourceDocument) + , m_suggestionData(suggestion) +{ + const QString refactoredText = suggestion.text; + + int startPos = suggestion.range.begin.toPositionInDocument(sourceDocument); + int endPos = suggestion.range.end.toPositionInDocument(sourceDocument); + startPos = qBound(0, startPos, sourceDocument->characterCount()); + endPos = qBound(0, endPos, sourceDocument->characterCount()); + + QTextCursor startCursor(sourceDocument); + startCursor.setPosition(startPos); + + if (startPos == endPos) { + QTextBlock block = startCursor.block(); + QString blockText = block.text(); + int startPosInBlock = startCursor.positionInBlock(); + + QString leftText = blockText.left(startPosInBlock); + QString rightText = blockText.mid(startPosInBlock); + + QString displayText = leftText + refactoredText + rightText; + replacementDocument()->setPlainText(displayText); + + } else { + QTextCursor fullLinesCursor(sourceDocument); + fullLinesCursor.setPosition(startPos); + fullLinesCursor.movePosition(QTextCursor::StartOfBlock); + int fullLinesStart = fullLinesCursor.position(); + + fullLinesCursor.setPosition(endPos); + fullLinesCursor.movePosition(QTextCursor::EndOfBlock); + int fullLinesEnd = fullLinesCursor.position(); + + fullLinesCursor.setPosition(fullLinesStart); + fullLinesCursor.setPosition(fullLinesEnd, QTextCursor::KeepAnchor); + QString fullLinesText = fullLinesCursor.selectedText(); + fullLinesText.replace(QChar(0x2029), "\n"); + + QString oldIndent = extractLeadingWhitespace(fullLinesText); + QString newIndent = extractLeadingWhitespace(refactoredText); + + QString displayText = refactoredText; + if (newIndent.length() < oldIndent.length()) { + QString indentDiff = oldIndent.left(oldIndent.length() - newIndent.length()); + QStringList lines = refactoredText.split('\n'); + if (!lines.isEmpty() && !lines[0].trimmed().isEmpty()) { + lines[0] = indentDiff + lines[0]; + displayText = lines.join('\n'); + } + } + + replacementDocument()->setPlainText(displayText); + } +} + +bool RefactorSuggestion::apply() +{ + const QString text = m_suggestionData.text; + const Utils::Text::Range range = m_suggestionData.range; + + const QTextCursor startCursor = range.begin.toTextCursor(sourceDocument()); + const QTextCursor endCursor = range.end.toTextCursor(sourceDocument()); + + const int startPos = startCursor.position(); + const int endPos = endCursor.position(); + + QTextCursor editCursor(sourceDocument()); + editCursor.beginEditBlock(); + + if (startPos == endPos) { + editCursor.setPosition(startPos); + editCursor.insertText(text); + } else { + editCursor.setPosition(startPos); + editCursor.setPosition(endPos, QTextCursor::KeepAnchor); + QString selectedText = editCursor.selectedText(); + selectedText.replace(QChar(0x2029), "\n"); + + QString oldIndent = extractLeadingWhitespace(selectedText); + QString newIndent = extractLeadingWhitespace(text); + + QString textToInsert = text; + if (newIndent.length() < oldIndent.length()) { + QString indentDiff = oldIndent.left(oldIndent.length() - newIndent.length()); + QStringList lines = text.split('\n'); + if (!lines.isEmpty() && !lines[0].trimmed().isEmpty()) { + lines[0] = indentDiff + lines[0]; + textToInsert = lines.join('\n'); + } + } + + editCursor.setPosition(startPos); + editCursor.setPosition(endPos, QTextCursor::KeepAnchor); + editCursor.removeSelectedText(); + editCursor.insertText(textToInsert); + } + + editCursor.endEditBlock(); + return true; +} + +bool RefactorSuggestion::applyWord(TextEditor::TextEditorWidget *widget) +{ + Q_UNUSED(widget) + return apply(); +} + +bool RefactorSuggestion::applyLine(TextEditor::TextEditorWidget *widget) +{ + Q_UNUSED(widget) + return apply(); +} + +} // namespace QodeAssist + diff --git a/RefactorSuggestion.hpp b/RefactorSuggestion.hpp new file mode 100644 index 0000000..c2aeb5b --- /dev/null +++ b/RefactorSuggestion.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024-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 + +namespace QodeAssist { + +/** + * @brief Persistent refactoring suggestion that displays code changes inline + * + * Unlike LLMSuggestion which supports partial acceptance (word/line), + * RefactorSuggestion is designed to show complete refactoring results + * that must be either fully accepted or rejected by the user. + */ +class RefactorSuggestion : public TextEditor::TextSuggestion +{ +public: + /** + * @brief Constructs a refactoring suggestion + * @param suggestion Suggestion data (range, position, text) + * @param sourceDocument The document where suggestion will be displayed + */ + RefactorSuggestion(const Data &suggestion, QTextDocument *sourceDocument); + + /** + * @brief Applies the full refactoring suggestion with smart overlapping + * @return true if suggestion was applied successfully + */ + bool apply() override; + + /** + * @brief Disabled: Word-by-word acceptance not supported for refactoring + */ + bool applyWord(TextEditor::TextEditorWidget *widget) override; + + /** + * @brief Disabled: Line-by-line acceptance not supported for refactoring + */ + bool applyLine(TextEditor::TextEditorWidget *widget) override; + +private: + Data m_suggestionData; +}; + +} // namespace QodeAssist + diff --git a/RefactorSuggestionHoverHandler.cpp b/RefactorSuggestionHoverHandler.cpp new file mode 100644 index 0000000..86fe7d1 --- /dev/null +++ b/RefactorSuggestionHoverHandler.cpp @@ -0,0 +1,227 @@ +/* + * 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 "RefactorSuggestionHoverHandler.hpp" +#include "RefactorSuggestion.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace QodeAssist { + +RefactorSuggestionHoverHandler::RefactorSuggestionHoverHandler() +{ + setPriority(Priority_Suggestion); +} + +void RefactorSuggestionHoverHandler::setSuggestionRange(const Utils::Text::Range &range) +{ + m_suggestionRange = range; + m_hasSuggestion = true; +} + +void RefactorSuggestionHoverHandler::clearSuggestionRange() +{ + m_hasSuggestion = false; +} + +void RefactorSuggestionHoverHandler::identifyMatch( + TextEditor::TextEditorWidget *editorWidget, + int pos, + ReportPriority report) +{ + LOG_MESSAGE(QString("RefactorSuggestionHoverHandler::identifyMatch pos=%1, hasSuggestion=%2, suggestionVisible=%3") + .arg(pos) + .arg(m_hasSuggestion) + .arg(editorWidget->suggestionVisible())); + + QScopeGuard cleanup([&] { report(Priority_None); }); + + if (!editorWidget->suggestionVisible()) { + LOG_MESSAGE("RefactorSuggestionHoverHandler: No suggestion visible"); + return; + } + + QTextCursor cursor(editorWidget->document()); + cursor.setPosition(pos); + m_block = cursor.block(); + + // Check if this block has a suggestion +#if QODEASSIST_QT_CREATOR_VERSION_MAJOR >= 17 + auto *suggestion = dynamic_cast( + TextEditor::TextBlockUserData::suggestion(m_block)); +#else + auto *userData = TextEditor::TextDocumentLayout::textUserData(m_block); + if (!userData) { + LOG_MESSAGE("RefactorSuggestionHoverHandler: No user data in block"); + return; + } + + auto *suggestion = dynamic_cast(userData->suggestion()); +#endif + + if (!suggestion) { + LOG_MESSAGE("RefactorSuggestionHoverHandler: No RefactorSuggestion in block"); + return; + } + + LOG_MESSAGE("RefactorSuggestionHoverHandler: Found RefactorSuggestion, reporting Priority_Suggestion"); + cleanup.dismiss(); + report(Priority_Suggestion); +} + +void RefactorSuggestionHoverHandler::operateTooltip( + TextEditor::TextEditorWidget *editorWidget, + const QPoint &point) +{ + Q_UNUSED(point) + +#if QODEASSIST_QT_CREATOR_VERSION_MAJOR >= 17 + auto *suggestion = dynamic_cast( + TextEditor::TextBlockUserData::suggestion(m_block)); +#else + auto *userData = TextEditor::TextDocumentLayout::textUserData(m_block); + if (!userData) { + LOG_MESSAGE("RefactorSuggestionHoverHandler::operateTooltip: No user data in block"); + return; + } + + auto *suggestion = dynamic_cast(userData->suggestion()); +#endif + + if (!suggestion) { + LOG_MESSAGE("RefactorSuggestionHoverHandler::operateTooltip: No suggestion in block"); + return; + } + + LOG_MESSAGE("RefactorSuggestionHoverHandler::operateTooltip: Creating tooltip widget"); + + // Create compact widget with buttons + auto *widget = new QWidget(); + auto *layout = new QHBoxLayout(widget); + layout->setContentsMargins(4, 3, 4, 3); + layout->setSpacing(6); + + // Get theme colors + const QColor normalBg = Utils::creatorColor(Utils::Theme::BackgroundColorNormal); + const QColor hoverBg = Utils::creatorColor(Utils::Theme::BackgroundColorHover); + const QColor selectedBg = Utils::creatorColor(Utils::Theme::BackgroundColorSelected); + const QColor textColor = Utils::creatorColor(Utils::Theme::TextColorNormal); + const QColor borderColor = Utils::creatorColor(Utils::Theme::SplitterColor); + const QColor successColor = Utils::creatorColor(Utils::Theme::TextColorNormal); + const QColor errorColor = Utils::creatorColor(Utils::Theme::TextColorError); + + auto *applyButton = new QPushButton("✓ Apply", widget); + applyButton->setFocusPolicy(Qt::NoFocus); + applyButton->setToolTip("Apply refactoring (Tab)"); + applyButton->setCursor(Qt::PointingHandCursor); + applyButton->setStyleSheet(QString( + "QPushButton {" + " background-color: %1;" + " color: %2;" + " border: 1px solid %3;" + " border-radius: 3px;" + " padding: 4px 12px;" + " font-weight: bold;" + " font-size: 11px;" + " min-width: 60px;" + "}" + "QPushButton:hover {" + " background-color: %4;" + " border-color: %2;" + "}" + "QPushButton:pressed {" + " background-color: %5;" + "}") + .arg(selectedBg.name()) + .arg(successColor.name()) + .arg(borderColor.name()) + .arg(selectedBg.lighter(110).name()) + .arg(selectedBg.darker(110).name())); + QObject::connect(applyButton, &QPushButton::clicked, widget, [this]() { + Utils::ToolTip::hide(); + if (m_applyCallback) { + m_applyCallback(); + } + }); + + auto *dismissButton = new QPushButton("✕ Dismiss", widget); + dismissButton->setFocusPolicy(Qt::NoFocus); + dismissButton->setToolTip("Dismiss refactoring (Esc)"); + dismissButton->setCursor(Qt::PointingHandCursor); + dismissButton->setStyleSheet(QString( + "QPushButton {" + " background-color: %1;" + " color: %2;" + " border: 1px solid %3;" + " border-radius: 3px;" + " padding: 4px 12px;" + " font-size: 11px;" + " min-width: 60px;" + "}" + "QPushButton:hover {" + " background-color: %4;" + " color: %5;" + " border-color: %5;" + "}" + "QPushButton:pressed {" + " background-color: %6;" + "}") + .arg(normalBg.name()) + .arg(textColor.name()) + .arg(borderColor.name()) + .arg(hoverBg.name()) + .arg(errorColor.name()) + .arg(hoverBg.darker(110).name())); + QObject::connect(dismissButton, &QPushButton::clicked, widget, [this]() { + Utils::ToolTip::hide(); + if (m_dismissCallback) { + m_dismissCallback(); + } + }); + + layout->addWidget(applyButton); + layout->addWidget(dismissButton); + + // Position tooltip above cursor, like standard Qt Creator suggestions + const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor()); + QPoint pos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft()) + - Utils::ToolTip::offsetFromPosition(); + pos.ry() -= widget->sizeHint().height(); + + LOG_MESSAGE(QString("RefactorSuggestionHoverHandler::operateTooltip: Showing tooltip at (%1, %2)") + .arg(pos.x()).arg(pos.y())); + + Utils::ToolTip::show(pos, widget, editorWidget); +} + +} // namespace QodeAssist + diff --git a/RefactorSuggestionHoverHandler.hpp b/RefactorSuggestionHoverHandler.hpp new file mode 100644 index 0000000..81b5e7a --- /dev/null +++ b/RefactorSuggestionHoverHandler.hpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include +#include + +#include +#include + +namespace TextEditor { +class TextEditorWidget; +} + +namespace QodeAssist { + +/** + * @brief Hover handler for refactoring suggestions + * + * Shows interactive tooltip with Apply/Dismiss buttons when hovering over + * a refactoring suggestion in the editor. + */ +class RefactorSuggestionHoverHandler : public TextEditor::BaseHoverHandler +{ +public: + using ApplyCallback = std::function; + using DismissCallback = std::function; + + RefactorSuggestionHoverHandler(); + + void setSuggestionRange(const Utils::Text::Range &range); + void clearSuggestionRange(); + bool hasSuggestion() const { return m_hasSuggestion; } + + void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); } + void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); } + +protected: + void identifyMatch(TextEditor::TextEditorWidget *editorWidget, + int pos, + ReportPriority report) override; + + void operateTooltip(TextEditor::TextEditorWidget *editorWidget, + const QPoint &point) override; + +private: + Utils::Text::Range m_suggestionRange; + bool m_hasSuggestion = false; + ApplyCallback m_applyCallback; + DismissCallback m_dismissCallback; + QTextBlock m_block; +}; + +} // namespace QodeAssist + diff --git a/llmcore/Provider.hpp b/llmcore/Provider.hpp index 9d47ad9..502ed61 100644 --- a/llmcore/Provider.hpp +++ b/llmcore/Provider.hpp @@ -53,7 +53,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) = 0; virtual QList getInstalledModels(const QString &url) = 0; virtual QList validateRequest(const QJsonObject &request, TemplateType type) = 0; diff --git a/providers/ClaudeProvider.cpp b/providers/ClaudeProvider.cpp index fcf3434..e548e60 100644 --- a/providers/ClaudeProvider.cpp +++ b/providers/ClaudeProvider.cpp @@ -30,6 +30,7 @@ #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" @@ -76,7 +77,8 @@ void ClaudeProvider::prepareRequest( LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) { if (!prompt->isSupportProvider(providerID())) { LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); @@ -93,20 +95,33 @@ void ClaudeProvider::prepareRequest( request["stream"] = true; }; + auto applyThinkingMode = [&request](const auto &settings) { + QJsonObject thinkingObj; + thinkingObj["type"] = "enabled"; + thinkingObj["budget_tokens"] = settings.thinkingBudgetTokens(); + request["thinking"] = thinkingObj; + request["max_tokens"] = settings.thinkingMaxTokens(); + request["temperature"] = 1.0; + }; + if (type == LLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); request["temperature"] = Settings::codeCompletionSettings().temperature(); + } else if (type == LLMCore::RequestType::QuickRefactoring) { + const auto &qrSettings = Settings::quickRefactorSettings(); + applyModelParams(qrSettings); + + if (isThinkingEnabled) { + applyThinkingMode(qrSettings); + } else { + request["temperature"] = qrSettings.temperature(); + } } else { const auto &chatSettings = Settings::chatAssistantSettings(); applyModelParams(chatSettings); - if (chatSettings.enableThinkingMode()) { - QJsonObject thinkingObj; - thinkingObj["type"] = "enabled"; - thinkingObj["budget_tokens"] = chatSettings.thinkingBudgetTokens(); - request["thinking"] = thinkingObj; - request["max_tokens"] = chatSettings.thinkingMaxTokens(); - request["temperature"] = 1.0; + if (isThinkingEnabled) { + applyThinkingMode(chatSettings); } else { request["temperature"] = chatSettings.temperature(); } diff --git a/providers/ClaudeProvider.hpp b/providers/ClaudeProvider.hpp index 61e25b7..a65d348 100644 --- a/providers/ClaudeProvider.hpp +++ b/providers/ClaudeProvider.hpp @@ -42,7 +42,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) override; + bool isToolsEnabled, + bool isThinkingEnabled) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QString apiKey() const override; diff --git a/providers/GoogleAIProvider.cpp b/providers/GoogleAIProvider.cpp index 5b4268d..6ba4448 100644 --- a/providers/GoogleAIProvider.cpp +++ b/providers/GoogleAIProvider.cpp @@ -30,6 +30,7 @@ #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" @@ -76,7 +77,8 @@ void GoogleAIProvider::prepareRequest( LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) { if (!prompt->isSupportProvider(providerID())) { LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); @@ -97,49 +99,43 @@ void GoogleAIProvider::prepareRequest( request["generationConfig"] = generationConfig; }; + auto applyThinkingMode = [&request](const auto &settings) { + QJsonObject generationConfig; + generationConfig["maxOutputTokens"] = settings.thinkingMaxTokens(); + + if (settings.useTopP()) + generationConfig["topP"] = settings.topP(); + if (settings.useTopK()) + generationConfig["topK"] = settings.topK(); + + generationConfig["temperature"] = 1.0; + + QJsonObject thinkingConfig; + thinkingConfig["includeThoughts"] = true; + int budgetTokens = settings.thinkingBudgetTokens(); + if (budgetTokens != -1) { + thinkingConfig["thinkingBudget"] = budgetTokens; + } + + generationConfig["thinkingConfig"] = thinkingConfig; + request["generationConfig"] = generationConfig; + }; + if (type == LLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); + } else if (type == LLMCore::RequestType::QuickRefactoring) { + const auto &qrSettings = Settings::quickRefactorSettings(); + + if (isThinkingEnabled) { + applyThinkingMode(qrSettings); + } else { + applyModelParams(qrSettings); + } } else { const auto &chatSettings = Settings::chatAssistantSettings(); - - if (chatSettings.enableThinkingMode()) { - QJsonObject generationConfig; - generationConfig["maxOutputTokens"] = chatSettings.thinkingMaxTokens(); - - if (chatSettings.useTopP()) - generationConfig["topP"] = chatSettings.topP(); - if (chatSettings.useTopK()) - generationConfig["topK"] = chatSettings.topK(); - - // Set temperature to 1.0 for thinking mode - generationConfig["temperature"] = 1.0; - - // Add thinkingConfig - QJsonObject thinkingConfig; - int budgetTokens = chatSettings.thinkingBudgetTokens(); - - // Dynamic thinking: -1 (let model decide) - // Disabled: 0 (no thinking) - // Custom budget: positive integer - if (budgetTokens == -1) { - // Dynamic thinking - omit budget to let model decide - thinkingConfig["includeThoughts"] = true; - } else if (budgetTokens == 0) { - // Disabled thinking - thinkingConfig["thinkingBudget"] = 0; - thinkingConfig["includeThoughts"] = false; - } else { - // Custom budget - thinkingConfig["thinkingBudget"] = budgetTokens; - thinkingConfig["includeThoughts"] = true; - } - - generationConfig["thinkingConfig"] = thinkingConfig; - request["generationConfig"] = generationConfig; - - LOG_MESSAGE(QString("Google AI thinking mode enabled: budget=%1 tokens, maxTokens=%2") - .arg(budgetTokens) - .arg(chatSettings.thinkingMaxTokens())); + + if (isThinkingEnabled) { + applyThinkingMode(chatSettings); } else { applyModelParams(chatSettings); } diff --git a/providers/GoogleAIProvider.hpp b/providers/GoogleAIProvider.hpp index 851126e..8795e91 100644 --- a/providers/GoogleAIProvider.hpp +++ b/providers/GoogleAIProvider.hpp @@ -41,7 +41,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) override; + bool isToolsEnabled, + bool isThinkingEnabled) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QString apiKey() const override; diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp index 1e03953..2ab2dee 100644 --- a/providers/LMStudioProvider.cpp +++ b/providers/LMStudioProvider.cpp @@ -23,6 +23,7 @@ #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" @@ -223,7 +224,8 @@ void LMStudioProvider::prepareRequest( LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) { if (!prompt->isSupportProvider(providerID())) { LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); @@ -247,6 +249,8 @@ void LMStudioProvider::prepareRequest( if (type == LLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); + } else if (type == LLMCore::RequestType::QuickRefactoring) { + applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } diff --git a/providers/LMStudioProvider.hpp b/providers/LMStudioProvider.hpp index 5042a51..f4d3e35 100644 --- a/providers/LMStudioProvider.hpp +++ b/providers/LMStudioProvider.hpp @@ -41,7 +41,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) override; + bool isToolsEnabled, + bool isThinkingEnabled) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QString apiKey() const override; diff --git a/providers/LlamaCppProvider.cpp b/providers/LlamaCppProvider.cpp index 318f719..db16651 100644 --- a/providers/LlamaCppProvider.cpp +++ b/providers/LlamaCppProvider.cpp @@ -23,6 +23,7 @@ #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include @@ -74,7 +75,8 @@ void LlamaCppProvider::prepareRequest( LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) { if (!prompt->isSupportProvider(providerID())) { LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); @@ -98,6 +100,8 @@ void LlamaCppProvider::prepareRequest( if (type == LLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); + } else if (type == LLMCore::RequestType::QuickRefactoring) { + applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } diff --git a/providers/LlamaCppProvider.hpp b/providers/LlamaCppProvider.hpp index b104dac..2cb57c9 100644 --- a/providers/LlamaCppProvider.hpp +++ b/providers/LlamaCppProvider.hpp @@ -41,7 +41,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) override; + bool isToolsEnabled, + bool isThinkingEnabled) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QString apiKey() const override; diff --git a/providers/MistralAIProvider.cpp b/providers/MistralAIProvider.cpp index 7dde7ad..e774cfa 100644 --- a/providers/MistralAIProvider.cpp +++ b/providers/MistralAIProvider.cpp @@ -23,6 +23,7 @@ #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" @@ -244,7 +245,8 @@ void MistralAIProvider::prepareRequest( LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) { if (!prompt->isSupportProvider(providerID())) { LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); @@ -268,6 +270,8 @@ void MistralAIProvider::prepareRequest( if (type == LLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); + } else if (type == LLMCore::RequestType::QuickRefactoring) { + applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } diff --git a/providers/MistralAIProvider.hpp b/providers/MistralAIProvider.hpp index bb41c14..7ef1d90 100644 --- a/providers/MistralAIProvider.hpp +++ b/providers/MistralAIProvider.hpp @@ -41,7 +41,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) override; + bool isToolsEnabled, + bool isThinkingEnabled) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QString apiKey() const override; diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index 3e91b00..7b5ba27 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -29,6 +29,7 @@ #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" @@ -75,7 +76,8 @@ void OllamaProvider::prepareRequest( LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) { if (!prompt->isSupportProvider(providerID())) { LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); @@ -104,6 +106,8 @@ void OllamaProvider::prepareRequest( if (type == LLMCore::RequestType::CodeCompletion) { applySettings(Settings::codeCompletionSettings()); + } else if (type == LLMCore::RequestType::QuickRefactoring) { + applySettings(Settings::quickRefactorSettings()); } else { applySettings(Settings::chatAssistantSettings()); } diff --git a/providers/OllamaProvider.hpp b/providers/OllamaProvider.hpp index 84fb2b3..62c4cdd 100644 --- a/providers/OllamaProvider.hpp +++ b/providers/OllamaProvider.hpp @@ -42,7 +42,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) override; + bool isToolsEnabled, + bool isThinkingEnabled) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QString apiKey() const override; diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp index 507a5c2..85026ce 100644 --- a/providers/OpenAICompatProvider.cpp +++ b/providers/OpenAICompatProvider.cpp @@ -23,6 +23,7 @@ #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" @@ -74,7 +75,8 @@ void OpenAICompatProvider::prepareRequest( LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) { if (!prompt->isSupportProvider(providerID())) { LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); @@ -98,6 +100,8 @@ void OpenAICompatProvider::prepareRequest( if (type == LLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); + } else if (type == LLMCore::RequestType::QuickRefactoring) { + applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } diff --git a/providers/OpenAICompatProvider.hpp b/providers/OpenAICompatProvider.hpp index a84a907..79452e6 100644 --- a/providers/OpenAICompatProvider.hpp +++ b/providers/OpenAICompatProvider.hpp @@ -41,7 +41,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) override; + bool isToolsEnabled, + bool isThinkingEnabled) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QString apiKey() const override; diff --git a/providers/OpenAIProvider.cpp b/providers/OpenAIProvider.cpp index ace9785..92ff574 100644 --- a/providers/OpenAIProvider.cpp +++ b/providers/OpenAIProvider.cpp @@ -23,6 +23,7 @@ #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" @@ -75,7 +76,8 @@ void OpenAIProvider::prepareRequest( LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) + bool isToolsEnabled, + bool isThinkingEnabled) { if (!prompt->isSupportProvider(providerID())) { LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); @@ -118,6 +120,8 @@ void OpenAIProvider::prepareRequest( if (type == LLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); + } else if (type == LLMCore::RequestType::QuickRefactoring) { + applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } diff --git a/providers/OpenAIProvider.hpp b/providers/OpenAIProvider.hpp index 5daf5ff..7b37352 100644 --- a/providers/OpenAIProvider.hpp +++ b/providers/OpenAIProvider.hpp @@ -41,7 +41,8 @@ public: LLMCore::PromptTemplate *prompt, LLMCore::ContextData context, LLMCore::RequestType type, - bool isToolsEnabled) override; + bool isToolsEnabled, + bool isThinkingEnabled) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QString apiKey() const override; diff --git a/settings/ButtonAspect.hpp b/settings/ButtonAspect.hpp index caec95c..16f6bb9 100644 --- a/settings/ButtonAspect.hpp +++ b/settings/ButtonAspect.hpp @@ -22,6 +22,7 @@ #include #include #include +#include class ButtonAspect : public Utils::BaseAspect { @@ -36,6 +37,17 @@ public: { auto button = new QPushButton(m_buttonText); button->setVisible(m_visible); + + if (!m_icon.isNull()) { + button->setIcon(m_icon); + button->setText(""); // Clear text if icon is set + } + + if (m_isCompact) { + button->setMaximumWidth(30); + button->setToolTip(m_tooltip.isEmpty() ? m_buttonText : m_tooltip); + } + connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked); connect(this, &ButtonAspect::visibleChanged, button, &QPushButton::setVisible); parent.addItem(button); @@ -50,6 +62,9 @@ public: } QString m_buttonText; + QIcon m_icon; + QString m_tooltip; + bool m_isCompact = false; signals: void clicked(); diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt index 7646363..c65e064 100644 --- a/settings/CMakeLists.txt +++ b/settings/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(QodeAssistSettings STATIC SettingsTr.hpp CodeCompletionSettings.hpp CodeCompletionSettings.cpp ChatAssistantSettings.hpp ChatAssistantSettings.cpp + QuickRefactorSettings.hpp QuickRefactorSettings.cpp ToolsSettings.hpp ToolsSettings.cpp SettingsDialog.hpp SettingsDialog.cpp ProjectSettings.hpp ProjectSettings.cpp diff --git a/settings/GeneralSettings.cpp b/settings/GeneralSettings.cpp index 28295c3..2bf7583 100644 --- a/settings/GeneralSettings.cpp +++ b/settings/GeneralSettings.cpp @@ -21,11 +21,12 @@ #include #include -#include #include #include #include +#include #include +#include #include #include #include @@ -117,7 +118,6 @@ GeneralSettings::GeneralSettings() ccTemplateDescription.setDisplayStyle(Utils::StringAspect::TextEditDisplay); ccTemplateDescription.setReadOnly(true); ccTemplateDescription.setDefaultValue(""); - ccTemplateDescription.setLabelText(TrConstants::CURRENT_TEMPLATE_DESCRIPTION); // preset1 specifyPreset1.setSettingsKey(Constants::CC_SPECIFY_PRESET1); @@ -203,7 +203,56 @@ GeneralSettings::GeneralSettings() caTemplateDescription.setDisplayStyle(Utils::StringAspect::TextEditDisplay); caTemplateDescription.setReadOnly(true); caTemplateDescription.setDefaultValue(""); - caTemplateDescription.setLabelText(TrConstants::CURRENT_TEMPLATE_DESCRIPTION); + + // quick refactor settings + initStringAspect(qrProvider, Constants::QR_PROVIDER, TrConstants::PROVIDER, "Ollama"); + qrProvider.setReadOnly(true); + qrSelectProvider.m_buttonText = TrConstants::SELECT; + + initStringAspect(qrModel, Constants::QR_MODEL, TrConstants::MODEL, "qwen2.5-coder:7b"); + qrModel.setHistoryCompleter(Constants::QR_MODEL_HISTORY); + qrSelectModel.m_buttonText = TrConstants::SELECT; + + initStringAspect(qrTemplate, Constants::QR_TEMPLATE, TrConstants::TEMPLATE, "Ollama Chat"); + qrTemplate.setReadOnly(true); + + qrSelectTemplate.m_buttonText = TrConstants::SELECT; + + initStringAspect(qrUrl, Constants::QR_URL, TrConstants::URL, "http://localhost:11434"); + qrUrl.setHistoryCompleter(Constants::QR_URL_HISTORY); + qrSetUrl.m_buttonText = TrConstants::SELECT; + + qrEndpointMode.setSettingsKey(Constants::QR_ENDPOINT_MODE); + qrEndpointMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); + qrEndpointMode.addOption("Auto"); + qrEndpointMode.addOption("Custom"); + qrEndpointMode.addOption("FIM"); + qrEndpointMode.addOption("Chat"); + qrEndpointMode.setDefaultValue("Auto"); + + initStringAspect(qrCustomEndpoint, Constants::QR_CUSTOM_ENDPOINT, TrConstants::ENDPOINT_MODE, ""); + qrCustomEndpoint.setHistoryCompleter(Constants::QR_CUSTOM_ENDPOINT_HISTORY); + + qrStatus.setDisplayStyle(Utils::StringAspect::LabelDisplay); + qrStatus.setLabelText(TrConstants::STATUS); + qrStatus.setDefaultValue(""); + qrTest.m_buttonText = TrConstants::TEST; + + qrTemplateDescription.setDisplayStyle(Utils::StringAspect::TextEditDisplay); + qrTemplateDescription.setReadOnly(true); + qrTemplateDescription.setDefaultValue(""); + + ccShowTemplateInfo.m_icon = Utils::Icons::INFO.icon(); + ccShowTemplateInfo.m_tooltip = Tr::tr("Show template information"); + ccShowTemplateInfo.m_isCompact = true; + + caShowTemplateInfo.m_icon = Utils::Icons::INFO.icon(); + caShowTemplateInfo.m_tooltip = Tr::tr("Show template information"); + caShowTemplateInfo.m_isCompact = true; + + qrShowTemplateInfo.m_icon = Utils::Icons::INFO.icon(); + qrShowTemplateInfo.m_tooltip = Tr::tr("Show template information"); + qrShowTemplateInfo.m_isCompact = true; readSettings(); @@ -215,6 +264,7 @@ GeneralSettings::GeneralSettings() ccCustomEndpoint.setEnabled(ccEndpointMode.stringValue() == "Custom"); ccPreset1CustomEndpoint.setEnabled(ccPreset1EndpointMode.stringValue() == "Custom"); caCustomEndpoint.setEnabled(caEndpointMode.stringValue() == "Custom"); + qrCustomEndpoint.setEnabled(qrEndpointMode.stringValue() == "Custom"); setLayouter([this]() { using namespace Layouting; @@ -224,7 +274,7 @@ GeneralSettings::GeneralSettings() ccGrid.addRow({ccUrl, ccSetUrl}); ccGrid.addRow({ccCustomEndpoint, ccEndpointMode}); ccGrid.addRow({ccModel, ccSelectModel}); - ccGrid.addRow({ccTemplate, ccSelectTemplate}); + ccGrid.addRow({ccTemplate, ccSelectTemplate, ccShowTemplateInfo}); auto ccPreset1Grid = Grid{}; ccPreset1Grid.addRow({ccPreset1Provider, ccPreset1SelectProvider}); @@ -238,21 +288,27 @@ GeneralSettings::GeneralSettings() caGrid.addRow({caUrl, caSetUrl}); caGrid.addRow({caCustomEndpoint, caEndpointMode}); caGrid.addRow({caModel, caSelectModel}); - caGrid.addRow({caTemplate, caSelectTemplate}); + caGrid.addRow({caTemplate, caSelectTemplate, caShowTemplateInfo}); + + auto qrGrid = Grid{}; + qrGrid.addRow({qrProvider, qrSelectProvider}); + qrGrid.addRow({qrUrl, qrSetUrl}); + qrGrid.addRow({qrCustomEndpoint, qrEndpointMode}); + qrGrid.addRow({qrModel, qrSelectModel}); + qrGrid.addRow({qrTemplate, qrSelectTemplate, qrShowTemplateInfo}); auto ccGroup = Group{ title(TrConstants::CODE_COMPLETION), Column{ ccGrid, - ccTemplateDescription, Row{specifyPreset1, preset1Language, Stretch{1}}, ccPreset1Grid}}; auto caGroup = Group{ - title(TrConstants::CHAT_ASSISTANT), - Column{ - caGrid, - caTemplateDescription}}; + title(TrConstants::CHAT_ASSISTANT), Column{caGrid}}; + + auto qrGroup = Group{ + title(TrConstants::QUICK_REFACTOR), Column{qrGrid}}; auto rootLayout = Column{ Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}}, @@ -262,6 +318,8 @@ GeneralSettings::GeneralSettings() ccGroup, Space{8}, caGroup, + Space{8}, + qrGroup, Stretch{1}}; return rootLayout; @@ -307,6 +365,9 @@ void GeneralSettings::showModelsNotFoundDialog(Utils::StringAspect &aspect) } else if (&aspect == &caModel) { providerButton = &caSelectProvider; urlButton = &caSetUrl; + } else if (&aspect == &qrModel) { + providerButton = &qrSelectProvider; + urlButton = &qrSetUrl; } if (providerButton && urlButton) { @@ -357,7 +418,8 @@ void GeneralSettings::showModelsNotSupportedDialog(Utils::StringAspect &aspect) QString key = QString("CompleterHistory/") .append( (&aspect == &ccModel) ? Constants::CC_MODEL_HISTORY - : Constants::CA_MODEL_HISTORY); + : (&aspect == &caModel) ? Constants::CA_MODEL_HISTORY + : Constants::QR_MODEL_HISTORY); #if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0) QStringList historyList = Utils::QtcSettings().value(Utils::Key(key.toLocal8Bit())).toStringList(); @@ -399,7 +461,8 @@ void GeneralSettings::showUrlSelectionDialog( .append( (&aspect == &ccUrl) ? Constants::CC_URL_HISTORY : (&aspect == &ccPreset1Url) ? Constants::CC_PRESET1_URL_HISTORY - : Constants::CA_URL_HISTORY); + : (&aspect == &caUrl) ? Constants::CA_URL_HISTORY + : Constants::QR_URL_HISTORY); #if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0) QStringList historyList = Utils::QtcSettings().value(Utils::Key(key.toLocal8Bit())).toStringList(); @@ -431,6 +494,31 @@ void GeneralSettings::showUrlSelectionDialog( dialog.exec(); } +void GeneralSettings::showTemplateInfoDialog(const Utils::StringAspect &descriptionAspect, const QString &templateName) +{ + SettingsDialog dialog(Tr::tr("Template Information")); + dialog.addLabel(QString("%1: %2").arg(Tr::tr("Template"), templateName)); + dialog.addSpacing(); + + auto *descriptionLabel = new QLabel(Tr::tr("Description:")); + dialog.layout()->addWidget(descriptionLabel); + + auto *textEdit = new QTextEdit(); + textEdit->setReadOnly(true); + textEdit->setMinimumHeight(200); + textEdit->setMinimumWidth(500); + textEdit->setText(descriptionAspect.value()); + dialog.layout()->addWidget(textEdit); + + dialog.addSpacing(); + + auto *closeButton = new QPushButton(TrConstants::CLOSE); + connect(closeButton, &QPushButton::clicked, &dialog, &QDialog::accept); + dialog.buttonLayout()->addWidget(closeButton); + + dialog.exec(); +} + void GeneralSettings::updatePreset1Visiblity(bool state) { ccPreset1Provider.setVisible(specifyPreset1.volatileValue()); @@ -471,6 +559,22 @@ void GeneralSettings::setupConnections() caCustomEndpoint.setEnabled( caEndpointMode.volatileValue() == caEndpointMode.indexForDisplay("Custom")); }); + connect(&qrEndpointMode, &Utils::BaseAspect::volatileValueChanged, this, [this]() { + qrCustomEndpoint.setEnabled( + qrEndpointMode.volatileValue() == qrEndpointMode.indexForDisplay("Custom")); + }); + + connect(&ccShowTemplateInfo, &ButtonAspect::clicked, this, [this]() { + showTemplateInfoDialog(ccTemplateDescription, ccTemplate.value()); + }); + + connect(&caShowTemplateInfo, &ButtonAspect::clicked, this, [this]() { + showTemplateInfoDialog(caTemplateDescription, caTemplate.value()); + }); + + connect(&qrShowTemplateInfo, &ButtonAspect::clicked, this, [this]() { + showTemplateInfoDialog(qrTemplateDescription, qrTemplate.value()); + }); } void GeneralSettings::resetPageToDefaults() @@ -506,6 +610,12 @@ void GeneralSettings::resetPageToDefaults() resetAspect(ccPreset1CustomEndpoint); resetAspect(caEndpointMode); resetAspect(caCustomEndpoint); + resetAspect(qrProvider); + resetAspect(qrModel); + resetAspect(qrTemplate); + resetAspect(qrUrl); + resetAspect(qrEndpointMode); + resetAspect(qrCustomEndpoint); writeSettings(); } } diff --git a/settings/GeneralSettings.hpp b/settings/GeneralSettings.hpp index ebf848b..c96f3c0 100644 --- a/settings/GeneralSettings.hpp +++ b/settings/GeneralSettings.hpp @@ -20,9 +20,14 @@ #pragma once #include +#include #include "ButtonAspect.hpp" +namespace Utils { +class DetailsWidget; +} + namespace QodeAssist::LLMCore { class Provider; } @@ -102,6 +107,31 @@ public: Utils::StringAspect caTemplateDescription{this}; + // quick refactor settings + Utils::StringAspect qrProvider{this}; + ButtonAspect qrSelectProvider{this}; + + Utils::StringAspect qrModel{this}; + ButtonAspect qrSelectModel{this}; + + Utils::StringAspect qrTemplate{this}; + ButtonAspect qrSelectTemplate{this}; + + Utils::StringAspect qrUrl{this}; + ButtonAspect qrSetUrl{this}; + + Utils::SelectionAspect qrEndpointMode{this}; + Utils::StringAspect qrCustomEndpoint{this}; + + Utils::StringAspect qrStatus{this}; + ButtonAspect qrTest{this}; + + Utils::StringAspect qrTemplateDescription{this}; + + ButtonAspect ccShowTemplateInfo{this}; + ButtonAspect caShowTemplateInfo{this}; + ButtonAspect qrShowTemplateInfo{this}; + void showSelectionDialog( const QStringList &data, Utils::StringAspect &aspect, @@ -114,6 +144,8 @@ public: void showUrlSelectionDialog(Utils::StringAspect &aspect, const QStringList &predefinedUrls); + void showTemplateInfoDialog(const Utils::StringAspect &descriptionAspect, const QString &templateName); + void updatePreset1Visiblity(bool state); private: diff --git a/settings/QuickRefactorSettings.cpp b/settings/QuickRefactorSettings.cpp new file mode 100644 index 0000000..a188b2a --- /dev/null +++ b/settings/QuickRefactorSettings.cpp @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2024-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 "QuickRefactorSettings.hpp" + +#include +#include +#include +#include + +#include "SettingsConstants.hpp" +#include "SettingsTr.hpp" +#include "SettingsUtils.hpp" + +namespace QodeAssist::Settings { + +QuickRefactorSettings &quickRefactorSettings() +{ + static QuickRefactorSettings settings; + return settings; +} + +QuickRefactorSettings::QuickRefactorSettings() +{ + setAutoApply(false); + + setDisplayName(Tr::tr("Quick Refactor")); + + // General Parameters Settings + temperature.setSettingsKey(Constants::QR_TEMPERATURE); + temperature.setLabelText(Tr::tr("Temperature:")); + temperature.setDefaultValue(0.5); + temperature.setRange(0.0, 2.0); + temperature.setSingleStep(0.1); + + maxTokens.setSettingsKey(Constants::QR_MAX_TOKENS); + maxTokens.setLabelText(Tr::tr("Max Tokens:")); + maxTokens.setRange(-1, 200000); + maxTokens.setDefaultValue(2000); + + // Advanced Parameters + useTopP.setSettingsKey(Constants::QR_USE_TOP_P); + useTopP.setDefaultValue(false); + useTopP.setLabelText(Tr::tr("Top P:")); + + topP.setSettingsKey(Constants::QR_TOP_P); + topP.setDefaultValue(0.9); + topP.setRange(0.0, 1.0); + topP.setSingleStep(0.1); + + useTopK.setSettingsKey(Constants::QR_USE_TOP_K); + useTopK.setDefaultValue(false); + useTopK.setLabelText(Tr::tr("Top K:")); + + topK.setSettingsKey(Constants::QR_TOP_K); + topK.setDefaultValue(50); + topK.setRange(1, 1000); + + usePresencePenalty.setSettingsKey(Constants::QR_USE_PRESENCE_PENALTY); + usePresencePenalty.setDefaultValue(false); + usePresencePenalty.setLabelText(Tr::tr("Presence Penalty:")); + + presencePenalty.setSettingsKey(Constants::QR_PRESENCE_PENALTY); + presencePenalty.setDefaultValue(0.0); + presencePenalty.setRange(-2.0, 2.0); + presencePenalty.setSingleStep(0.1); + + useFrequencyPenalty.setSettingsKey(Constants::QR_USE_FREQUENCY_PENALTY); + useFrequencyPenalty.setDefaultValue(false); + useFrequencyPenalty.setLabelText(Tr::tr("Frequency Penalty:")); + + frequencyPenalty.setSettingsKey(Constants::QR_FREQUENCY_PENALTY); + frequencyPenalty.setDefaultValue(0.0); + frequencyPenalty.setRange(-2.0, 2.0); + frequencyPenalty.setSingleStep(0.1); + + // Ollama Settings + ollamaLivetime.setSettingsKey(Constants::QR_OLLAMA_LIVETIME); + ollamaLivetime.setToolTip( + Tr::tr("Time to suspend Ollama after completion request (in minutes), " + "Only Ollama, -1 to disable")); + ollamaLivetime.setLabelText("Livetime:"); + ollamaLivetime.setDefaultValue("5m"); + ollamaLivetime.setDisplayStyle(Utils::StringAspect::LineEditDisplay); + + contextWindow.setSettingsKey(Constants::QR_OLLAMA_CONTEXT_WINDOW); + contextWindow.setLabelText(Tr::tr("Context Window:")); + contextWindow.setRange(-1, 10000); + contextWindow.setDefaultValue(2048); + + useTools.setSettingsKey(Constants::QR_USE_TOOLS); + useTools.setLabelText(Tr::tr("Enable Tools")); + useTools.setToolTip( + Tr::tr("Enable AI tools/functions for quick refactoring (allows reading project files, " + "searching code, etc.)")); + useTools.setDefaultValue(false); + + useThinking.setSettingsKey(Constants::QR_USE_THINKING); + useThinking.setLabelText(Tr::tr("Enable Thinking Mode")); + useThinking.setToolTip( + Tr::tr("Enable extended thinking mode for complex refactoring tasks (supported by " + "compatible models like Claude and Google AI)")); + useThinking.setDefaultValue(false); + + thinkingBudgetTokens.setSettingsKey(Constants::QR_THINKING_BUDGET_TOKENS); + thinkingBudgetTokens.setLabelText(Tr::tr("Thinking Budget Tokens:")); + thinkingBudgetTokens.setToolTip( + Tr::tr("Number of tokens allocated for thinking process. Use -1 for dynamic thinking " + "(model decides), 0 to disable, or positive value for custom budget")); + thinkingBudgetTokens.setRange(-1, 100000); + thinkingBudgetTokens.setDefaultValue(10000); + + thinkingMaxTokens.setSettingsKey(Constants::QR_THINKING_MAX_TOKENS); + thinkingMaxTokens.setLabelText(Tr::tr("Thinking Max Output Tokens:")); + thinkingMaxTokens.setToolTip( + Tr::tr("Maximum output tokens when thinking mode is enabled (includes thinking + response)")); + thinkingMaxTokens.setRange(1000, 200000); + thinkingMaxTokens.setDefaultValue(16000); + + // Context Settings + readFullFile.setSettingsKey(Constants::QR_READ_FULL_FILE); + readFullFile.setLabelText(Tr::tr("Read Full File")); + readFullFile.setDefaultValue(false); + + readFileParts.setLabelText(Tr::tr("Read Strings Before Cursor:")); + readFileParts.setDefaultValue(true); + + readStringsBeforeCursor.setSettingsKey(Constants::QR_READ_STRINGS_BEFORE_CURSOR); + readStringsBeforeCursor.setLabelText(Tr::tr("Lines Before Cursor/Selection:")); + readStringsBeforeCursor.setToolTip( + Tr::tr("Number of lines to include before cursor or selection for context")); + readStringsBeforeCursor.setRange(0, 10000); + readStringsBeforeCursor.setDefaultValue(50); + + readStringsAfterCursor.setSettingsKey(Constants::QR_READ_STRINGS_AFTER_CURSOR); + readStringsAfterCursor.setLabelText(Tr::tr("Lines After Cursor/Selection:")); + readStringsAfterCursor.setToolTip( + Tr::tr("Number of lines to include after cursor or selection for context")); + readStringsAfterCursor.setRange(0, 10000); + readStringsAfterCursor.setDefaultValue(30); + + systemPrompt.setSettingsKey(Constants::QR_SYSTEM_PROMPT); + systemPrompt.setLabelText(Tr::tr("System Prompt:")); + systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay); + systemPrompt.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"); + + resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS; + + readSettings(); + + readFileParts.setValue(!readFullFile.value()); + + setupConnections(); + + setLayouter([this]() { + using namespace Layouting; + + auto genGrid = Grid{}; + genGrid.addRow({Row{temperature}}); + genGrid.addRow({Row{maxTokens}}); + + auto advancedGrid = Grid{}; + advancedGrid.addRow({useTopP, topP}); + advancedGrid.addRow({useTopK, topK}); + advancedGrid.addRow({usePresencePenalty, presencePenalty}); + advancedGrid.addRow({useFrequencyPenalty, frequencyPenalty}); + + auto ollamaGrid = Grid{}; + ollamaGrid.addRow({ollamaLivetime}); + ollamaGrid.addRow({contextWindow}); + + auto toolsGrid = Grid{}; + toolsGrid.addRow({useTools}); + toolsGrid.addRow({useThinking}); + toolsGrid.addRow({thinkingBudgetTokens}); + toolsGrid.addRow({thinkingMaxTokens}); + + auto contextGrid = Grid{}; + contextGrid.addRow({Row{readFullFile}}); + contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}}); + + return Column{ + Row{Stretch{1}, resetToDefaults}, + Space{8}, + Group{ + title(Tr::tr("General Parameters")), + Row{genGrid, Stretch{1}}, + }, + Space{8}, + Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}}, + Space{8}, + Group{title(Tr::tr("Tools Settings")), Column{Row{toolsGrid, Stretch{1}}}}, + Space{8}, + Group{title(Tr::tr("Context Settings")), Column{Row{contextGrid, Stretch{1}}}}, + Space{8}, + Group{title(Tr::tr("Prompt Settings")), Column{Row{systemPrompt}}}, + Space{8}, + Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}}, + Stretch{1}}; + }); +} + +void QuickRefactorSettings::setupConnections() +{ + connect( + &resetToDefaults, + &ButtonAspect::clicked, + this, + &QuickRefactorSettings::resetSettingsToDefaults); + + connect(&readFullFile, &Utils::BoolAspect::volatileValueChanged, this, [this]() { + if (readFullFile.volatileValue()) { + readFileParts.setValue(false); + writeSettings(); + } + }); + + connect(&readFileParts, &Utils::BoolAspect::volatileValueChanged, this, [this]() { + if (readFileParts.volatileValue()) { + readFullFile.setValue(false); + writeSettings(); + } + }); +} + +void QuickRefactorSettings::resetSettingsToDefaults() +{ + QMessageBox::StandardButton reply; + reply = QMessageBox::question( + Core::ICore::dialogParent(), + Tr::tr("Reset Settings"), + Tr::tr("Are you sure you want to reset all settings to default values?"), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + resetAspect(temperature); + resetAspect(maxTokens); + resetAspect(useTopP); + resetAspect(topP); + resetAspect(useTopK); + resetAspect(topK); + resetAspect(usePresencePenalty); + resetAspect(presencePenalty); + resetAspect(useFrequencyPenalty); + resetAspect(frequencyPenalty); + resetAspect(ollamaLivetime); + resetAspect(contextWindow); + resetAspect(useTools); + resetAspect(useThinking); + resetAspect(thinkingBudgetTokens); + resetAspect(thinkingMaxTokens); + resetAspect(readFullFile); + resetAspect(readFileParts); + resetAspect(readStringsBeforeCursor); + resetAspect(readStringsAfterCursor); + resetAspect(systemPrompt); + writeSettings(); + } +} + +class QuickRefactorSettingsPage : public Core::IOptionsPage +{ +public: + QuickRefactorSettingsPage() + { + setId(Constants::QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID); + setDisplayName(Tr::tr("Quick Refactor")); + setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY); + setSettingsProvider([] { return &quickRefactorSettings(); }); + } +}; + +const QuickRefactorSettingsPage quickRefactorSettingsPage; + +} // namespace QodeAssist::Settings + diff --git a/settings/QuickRefactorSettings.hpp b/settings/QuickRefactorSettings.hpp new file mode 100644 index 0000000..05f3bf4 --- /dev/null +++ b/settings/QuickRefactorSettings.hpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024-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 "ButtonAspect.hpp" + +namespace QodeAssist::Settings { + +class QuickRefactorSettings : public Utils::AspectContainer +{ +public: + QuickRefactorSettings(); + + ButtonAspect resetToDefaults{this}; + + // General Parameters Settings + Utils::DoubleAspect temperature{this}; + Utils::IntegerAspect maxTokens{this}; + + // Advanced Parameters + Utils::BoolAspect useTopP{this}; + Utils::DoubleAspect topP{this}; + + Utils::BoolAspect useTopK{this}; + Utils::IntegerAspect topK{this}; + + Utils::BoolAspect usePresencePenalty{this}; + Utils::DoubleAspect presencePenalty{this}; + + Utils::BoolAspect useFrequencyPenalty{this}; + Utils::DoubleAspect frequencyPenalty{this}; + + // Ollama Settings + Utils::StringAspect ollamaLivetime{this}; + Utils::IntegerAspect contextWindow{this}; + + // Tools Settings + Utils::BoolAspect useTools{this}; + + // Thinking Settings + Utils::BoolAspect useThinking{this}; + Utils::IntegerAspect thinkingBudgetTokens{this}; + Utils::IntegerAspect thinkingMaxTokens{this}; + + // Context Settings + Utils::BoolAspect readFullFile{this}; + Utils::BoolAspect readFileParts{this}; + Utils::IntegerAspect readStringsBeforeCursor{this}; + Utils::IntegerAspect readStringsAfterCursor{this}; + + // Prompt Settings + Utils::StringAspect systemPrompt{this}; + +private: + void setupConnections(); + void resetSettingsToDefaults(); +}; + +QuickRefactorSettings &quickRefactorSettings(); + +} // namespace QodeAssist::Settings + diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 5c80f46..e3d77cf 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -49,6 +49,17 @@ const char CA_ENDPOINT_MODE[] = "QodeAssist.caEndpointMode"; const char CA_CUSTOM_ENDPOINT[] = "QodeAssist.caCustomEndpoint"; const char CA_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.caCustomEndpointHistory"; +// quick refactor settings +const char QR_PROVIDER[] = "QodeAssist.qrProvider"; +const char QR_MODEL[] = "QodeAssist.qrModel"; +const char QR_MODEL_HISTORY[] = "QodeAssist.qrModelHistory"; +const char QR_TEMPLATE[] = "QodeAssist.qrTemplate"; +const char QR_URL[] = "QodeAssist.qrUrl"; +const char QR_URL_HISTORY[] = "QodeAssist.qrUrlHistory"; +const char QR_ENDPOINT_MODE[] = "QodeAssist.qrEndpointMode"; +const char QR_CUSTOM_ENDPOINT[] = "QodeAssist.qrCustomEndpoint"; +const char QR_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.qrCustomEndpointHistory"; + const char CC_SPECIFY_PRESET1[] = "QodeAssist.ccSpecifyPreset1"; const char CC_PRESET1_LANGUAGE[] = "QodeAssist.ccPreset1Language"; const char CC_PRESET1_PROVIDER[] = "QodeAssist.ccPreset1Provider"; @@ -98,14 +109,16 @@ const char QODE_ASSIST_CODE_COMPLETION_SETTINGS_PAGE_ID[] = "QodeAssist.2CodeCompletionSettingsPageId"; const char QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID[] = "QodeAssist.3ChatAssistantSettingsPageId"; -const char QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID[] = "QodeAssist.4ToolsSettingsPageId"; -const char QODE_ASSIST_CUSTOM_PROMPT_SETTINGS_PAGE_ID[] = "QodeAssist.5CustomPromptSettingsPageId"; +const char QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID[] + = "QodeAssist.4QuickRefactorSettingsPageId"; +const char QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID[] = "QodeAssist.5ToolsSettingsPageId"; +const char QODE_ASSIST_CUSTOM_PROMPT_SETTINGS_PAGE_ID[] = "QodeAssist.6CustomPromptSettingsPageId"; const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category"; const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist"; // Provider Settings Page ID -const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.6ProviderSettingsPageId"; +const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.7ProviderSettingsPageId"; // Provider API Keys const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey"; @@ -178,4 +191,26 @@ const char CA_CODE_FONT_SIZE[] = "QodeAssist.caCodeFontSize"; const char CA_TEXT_FORMAT[] = "QodeAssist.caTextFormat"; const char CA_CHAT_RENDERER[] = "QodeAssist.caChatRenderer"; +// quick refactor preset prompt settings +const char QR_TEMPERATURE[] = "QodeAssist.qrTemperature"; +const char QR_MAX_TOKENS[] = "QodeAssist.qrMaxTokens"; +const char QR_USE_TOP_P[] = "QodeAssist.qrUseTopP"; +const char QR_TOP_P[] = "QodeAssist.qrTopP"; +const char QR_USE_TOP_K[] = "QodeAssist.qrUseTopK"; +const char QR_TOP_K[] = "QodeAssist.qrTopK"; +const char QR_USE_PRESENCE_PENALTY[] = "QodeAssist.qrUsePresencePenalty"; +const char QR_PRESENCE_PENALTY[] = "QodeAssist.qrPresencePenalty"; +const char QR_USE_FREQUENCY_PENALTY[] = "QodeAssist.qrUseFrequencyPenalty"; +const char QR_FREQUENCY_PENALTY[] = "QodeAssist.qrFrequencyPenalty"; +const char QR_OLLAMA_LIVETIME[] = "QodeAssist.qrOllamaLivetime"; +const char QR_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.qrOllamaContextWindow"; +const char QR_USE_TOOLS[] = "QodeAssist.qrUseTools"; +const char QR_USE_THINKING[] = "QodeAssist.qrUseThinking"; +const char QR_THINKING_BUDGET_TOKENS[] = "QodeAssist.qrThinkingBudgetTokens"; +const char QR_THINKING_MAX_TOKENS[] = "QodeAssist.qrThinkingMaxTokens"; +const char QR_READ_FULL_FILE[] = "QodeAssist.qrReadFullFile"; +const char QR_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.qrReadStringsBeforeCursor"; +const char QR_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.qrReadStringsAfterCursor"; +const char QR_SYSTEM_PROMPT[] = "QodeAssist.qrSystemPrompt"; + } // namespace QodeAssist::Constants diff --git a/settings/SettingsTr.hpp b/settings/SettingsTr.hpp index affc9e3..fa4cd78 100644 --- a/settings/SettingsTr.hpp +++ b/settings/SettingsTr.hpp @@ -48,6 +48,7 @@ inline const char *ENDPOINT_MODE = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Endpoin inline const char *CODE_COMPLETION = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Code Completion"); inline const char *CHAT_ASSISTANT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Chat Assistant"); +inline const char *QUICK_REFACTOR = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Quick Refactor"); inline const char *RESET_SETTINGS = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Reset Settings"); inline const char *CONFIRMATION = QT_TRANSLATE_NOOP( "QtC::QodeAssist", "Are you sure you want to reset all settings to default values?"); diff --git a/test/LLMClientInterfaceTests.cpp b/test/LLMClientInterfaceTests.cpp index 8782ec1..bdbd7ee 100644 --- a/test/LLMClientInterfaceTests.cpp +++ b/test/LLMClientInterfaceTests.cpp @@ -68,7 +68,9 @@ public: QJsonObject &request, LLMCore::PromptTemplate *promptTemplate, LLMCore::ContextData context, - LLMCore::RequestType requestType) override + LLMCore::RequestType requestType, + bool isToolsEnabled, + bool isThinkingEnabled) override { promptTemplate->prepareRequest(request, context); } diff --git a/widgets/CompletionErrorHandler.cpp b/widgets/CompletionErrorHandler.cpp new file mode 100644 index 0000000..057dc3f --- /dev/null +++ b/widgets/CompletionErrorHandler.cpp @@ -0,0 +1,100 @@ +/* + * 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 "CompletionErrorHandler.hpp" + +#include +#include + +#include "ErrorWidget.hpp" + +namespace QodeAssist { + +void CompletionErrorHandler::showError( + TextEditor::TextEditorWidget *widget, + const QString &errorMessage, + int autoHideMs) +{ + m_widget = widget; + m_errorMessage = errorMessage; + + if (m_widget) { + const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor()); + m_errorPosition = m_widget->viewport()->mapToGlobal(cursorRect.topLeft()) + - Utils::ToolTip::offsetFromPosition(); + + identifyMatch(m_widget, m_widget->textCursor().position(), [this, autoHideMs](auto priority) { + if (priority != Priority_None) { + if (m_errorWidget) { + m_errorWidget->deleteLater(); + } + + m_errorWidget = new ErrorWidget(m_errorMessage, m_widget); + + const QRect cursorRect = m_widget->cursorRect(m_widget->textCursor()); + QPoint globalPos = m_widget->viewport()->mapToGlobal(cursorRect.topLeft()); + QPoint localPos = m_widget->mapFromGlobal(globalPos); + localPos.ry() -= m_errorWidget->height() + 5; + + if (localPos.y() < 0) { + localPos.ry() = cursorRect.bottom() + 5; + } + + m_errorWidget->move(localPos); + m_errorWidget->show(); + m_errorWidget->raise(); + + QObject::connect(m_errorWidget, &ErrorWidget::dismissed, m_errorWidget, [this]() { + hideError(); + }); + } + }); + } +} + +void CompletionErrorHandler::hideError() +{ + if (m_errorWidget) { + m_errorWidget->deleteLater(); + m_errorWidget = nullptr; + } + Utils::ToolTip::hideImmediately(); + m_errorMessage.clear(); +} + +void CompletionErrorHandler::identifyMatch( + TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) +{ + if (!editorWidget || m_errorMessage.isEmpty()) { + report(Priority_None); + return; + } + + report(Priority_Tooltip); +} + +void CompletionErrorHandler::operateTooltip( + TextEditor::TextEditorWidget *editorWidget, const QPoint &point) +{ + Q_UNUSED(editorWidget) + Q_UNUSED(point) +} + +} // namespace QodeAssist + diff --git a/widgets/CompletionErrorHandler.hpp b/widgets/CompletionErrorHandler.hpp new file mode 100644 index 0000000..47d62c5 --- /dev/null +++ b/widgets/CompletionErrorHandler.hpp @@ -0,0 +1,54 @@ +/* + * 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 + +namespace QodeAssist { + +class ErrorWidget; + +class CompletionErrorHandler : public TextEditor::BaseHoverHandler +{ +public: + void showError( + TextEditor::TextEditorWidget *widget, + const QString &errorMessage, + int autoHideMs = 5000); + + void hideError(); + + bool isErrorVisible() const { return !m_errorWidget.isNull(); } + +protected: + void identifyMatch( + TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override; + void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override; + +private: + QPointer m_widget; + QPointer m_errorWidget; + QString m_errorMessage; + QPoint m_errorPosition; +}; + +} // namespace QodeAssist + diff --git a/widgets/CompletionProgressHandler.cpp b/widgets/CompletionProgressHandler.cpp index 2536588..2b0da31 100644 --- a/widgets/CompletionProgressHandler.cpp +++ b/widgets/CompletionProgressHandler.cpp @@ -53,6 +53,10 @@ void CompletionProgressHandler::showProgress(TextEditor::TextEditorWidget *widge void CompletionProgressHandler::hideProgress() { + if (m_progressWidget) { + m_progressWidget->deleteLater(); + m_progressWidget = nullptr; + } Utils::ToolTip::hideImmediately(); } @@ -73,12 +77,24 @@ void CompletionProgressHandler::operateTooltip( if (!editorWidget) return; - auto progressWidget = new ProgressWidget(editorWidget); + if (m_progressWidget) { + delete m_progressWidget; + } - QPoint showPoint = point; - showPoint.ry() -= progressWidget->height(); - - Utils::ToolTip::show(showPoint, progressWidget, editorWidget); + m_progressWidget = new ProgressWidget(editorWidget); + + const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor()); + QPoint globalPos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft()); + QPoint localPos = editorWidget->mapFromGlobal(globalPos); + localPos.ry() -= m_progressWidget->height() + 5; + + if (localPos.y() < 0) { + localPos.ry() = cursorRect.bottom() + 5; + } + + m_progressWidget->move(localPos); + m_progressWidget->show(); + m_progressWidget->raise(); } } // namespace QodeAssist diff --git a/widgets/CompletionProgressHandler.hpp b/widgets/CompletionProgressHandler.hpp index 6f32e97..fb60857 100644 --- a/widgets/CompletionProgressHandler.hpp +++ b/widgets/CompletionProgressHandler.hpp @@ -24,6 +24,8 @@ namespace QodeAssist { +class ProgressWidget; + class CompletionProgressHandler : public TextEditor::BaseHoverHandler { public: @@ -37,6 +39,7 @@ protected: private: QPointer m_widget; + QPointer m_progressWidget; QPoint m_iconPosition; }; diff --git a/widgets/ErrorWidget.cpp b/widgets/ErrorWidget.cpp new file mode 100644 index 0000000..bf98b9d --- /dev/null +++ b/widgets/ErrorWidget.cpp @@ -0,0 +1,213 @@ +/* + * 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 "ErrorWidget.hpp" + +#include +#include +#include + +namespace QodeAssist { + +ErrorWidget::ErrorWidget(const QString &errorMessage, QWidget *parent, int autoHideMs) + : QWidget(parent) + , m_errorMessage(errorMessage) + , m_autoHideTimer(nullptr) + , m_isHovered(false) +{ + setupColors(); + setupIcon(); + + QFont errorFont = font(); + errorFont.setPointSize(qMax(8, errorFont.pointSize() - 2)); + setFont(errorFont); + + setFixedSize(calculateSize()); + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + if (autoHideMs > 0) { + m_autoHideTimer = new QTimer(this); + m_autoHideTimer->setSingleShot(true); + connect(m_autoHideTimer, &QTimer::timeout, this, [this]() { + if (!m_isHovered) { + emit dismissed(); + deleteLater(); + } + }); + m_autoHideTimer->start(autoHideMs); + } +} + +ErrorWidget::~ErrorWidget() +{ + if (m_autoHideTimer) { + m_autoHideTimer->stop(); + } +} + +void ErrorWidget::setErrorMessage(const QString &message) +{ + m_errorMessage = message; + QFont smallFont = font(); + smallFont.setPointSize(qMax(8, smallFont.pointSize() - 2)); + setFont(smallFont); + setFixedSize(calculateSize()); + update(); +} + +void ErrorWidget::setupColors() +{ + m_textColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal); + m_backgroundColor = Utils::creatorTheme()->color(Utils::Theme::BackgroundColorNormal); + m_errorColor = Utils::creatorTheme()->color(Utils::Theme::TextColorError); +} + +void ErrorWidget::setupIcon() +{ + // Create a smaller error icon (exclamation mark in a circle) + QPixmap pixmap(18, 18); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing); + + // Draw circle + painter.setPen(QPen(m_errorColor, 1.5)); + painter.setBrush(Qt::NoBrush); + painter.drawEllipse(1, 1, 16, 16); + + // Draw exclamation mark + painter.setPen(Qt::NoPen); + painter.setBrush(m_errorColor); + + // Vertical bar of exclamation + painter.drawRect(8, 4, 2, 8); + + // Dot of exclamation + painter.drawRect(8, 13, 2, 2); + + m_errorIcon = pixmap; +} + +QSize ErrorWidget::calculateSize() const +{ + QFontMetrics fm(font()); + + // Maximum width for the text area + const int maxTextWidth = 350; + const int iconWidth = 18; + const int padding = 8; + const int margin = 12; + + // Calculate text area with word wrapping + QRect textRect = fm.boundingRect( + 0, 0, maxTextWidth, 1000, + Qt::AlignLeft | Qt::TextWordWrap, + m_errorMessage + ); + + // Total width: margin + icon + padding + text + margin + int totalWidth = margin + iconWidth + padding + textRect.width() + margin; + + // Total height: larger of icon or text, plus vertical margins + int contentHeight = qMax(iconWidth, textRect.height()); + int totalHeight = contentHeight + margin * 2; + + return QSize(totalWidth, totalHeight); +} + +void ErrorWidget::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::TextAntialiasing); + + // Draw background with border + QColor bgColor = m_backgroundColor; + if (m_isHovered) { + bgColor = bgColor.lighter(110); + } + + QPainterPath path; + path.addRoundedRect(rect().adjusted(1, 1, -1, -1), 4, 4); + + painter.fillPath(path, bgColor); + painter.setPen(QPen(m_errorColor.lighter(150), 1)); + painter.drawPath(path); + + const int iconSize = 18; + const int padding = 8; + const int margin = 12; + + // Draw error icon + if (!m_errorIcon.isNull()) { + QRect iconRect(margin, margin, iconSize, iconSize); + painter.drawPixmap(iconRect, m_errorIcon); + } + + // Draw error message with word wrap + painter.setPen(m_textColor); + QRect textRect = rect().adjusted( + margin + iconSize + padding, // left + margin, // top + -margin, // right + -margin // bottom + ); + + painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, m_errorMessage); +} + +void ErrorWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + emit dismissed(); + deleteLater(); + } + QWidget::mousePressEvent(event); +} + +void ErrorWidget::enterEvent(QEnterEvent *event) +{ + m_isHovered = true; + update(); + + // Stop auto-hide timer while hovering + if (m_autoHideTimer && m_autoHideTimer->isActive()) { + m_autoHideTimer->stop(); + } + + QWidget::enterEvent(event); +} + +void ErrorWidget::leaveEvent(QEvent *event) +{ + m_isHovered = false; + update(); + + // Restart auto-hide timer when leaving + if (m_autoHideTimer) { + m_autoHideTimer->start(2000); // Give 2 more seconds after leaving + } + + QWidget::leaveEvent(event); +} + +} // namespace QodeAssist + diff --git a/widgets/ErrorWidget.hpp b/widgets/ErrorWidget.hpp new file mode 100644 index 0000000..f9c502d --- /dev/null +++ b/widgets/ErrorWidget.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include + +namespace QodeAssist { + +class ErrorWidget : public QWidget +{ + Q_OBJECT +public: + explicit ErrorWidget(const QString &errorMessage, QWidget *parent = nullptr, int autoHideMs = 5000); + ~ErrorWidget(); + + void setErrorMessage(const QString &message); + + QString errorMessage() const { return m_errorMessage; } + +signals: + void dismissed(); + +protected: + void paintEvent(QPaintEvent *) override; + void mousePressEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + QString m_errorMessage; + QTimer *m_autoHideTimer; + QColor m_textColor; + QColor m_backgroundColor; + QColor m_errorColor; + QPixmap m_errorIcon; + bool m_isHovered; + + void setupColors(); + void setupIcon(); + QSize calculateSize() const; +}; + +} // namespace QodeAssist +