refactor: Full rework quick refactor (#257)

This commit is contained in:
Petr Mironychev
2025-11-15 14:51:47 +01:00
committed by GitHub
parent 9ecd285d1d
commit 953774aaa8
45 changed files with 2002 additions and 125 deletions

View File

@@ -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

View File

@@ -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}};

View File

@@ -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]() {

View File

@@ -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;
}

View File

@@ -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
{

View File

@@ -24,7 +24,9 @@
#include "QodeAssistClient.hpp"
#include <QApplication>
#include <QInputDialog>
#include <QKeyEvent>
#include <QTimer>
#include <coreplugin/icore.h>
@@ -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<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) {
qDebug() << "setResponseCallback";
QTC_ASSERT(editor, return);
handleCompletions(response, editor);
});
@@ -243,9 +260,18 @@ 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())
requestPosition = requestParams->position().toPositionInDocument(editor->document());
@@ -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<LLMSuggestion>(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());
cursor.insertText(result.newText);
cursor.endEditBlock();
m_progressHandler.hideProgress();
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);
}
TextEditor::TextSuggestion::Data suggestionData{
Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
pos,
result.newText
};
editorWidget->insertSuggestion(
std::make_unique<RefactorSuggestion>(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

View File

@@ -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 <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
@@ -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;
};

View File

@@ -33,6 +33,7 @@
#include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp>
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 ? "<selection_end><cursor>" : "<selection_end>");
taggedContent.insert(
selStart, selStart == cursorPos ? "<cursor><selection_start>" : "<selection_start>");
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 {
taggedContent.insert(cursorPos, "<cursor>");
contextBefore = reader.getContextBefore(
startLine, startColumn, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1);
}
QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt();
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 += "<cursor><selection_start>" + selectedText + "<selection_end>";
} else {
taggedContent += "<selection_start>" + selectedText + "<selection_end><cursor>";
}
taggedContent += contextAfter;
} else {
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 + "<cursor>" + contextAfter;
}
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);
}
}

View File

@@ -38,6 +38,7 @@ struct RefactorResult
Utils::Text::Range insertRange;
bool success;
QString errorMessage;
TextEditor::TextEditorWidget *editor{nullptr};
};
class QuickRefactorHandler : public QObject

202
RefactorSuggestion.cpp Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#include "RefactorSuggestion.hpp"
#include "LLMSuggestion.hpp"
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <texteditor/texteditor.h>
#include <logger/Logger.hpp>
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

65
RefactorSuggestion.hpp Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <texteditor/texteditor.h>
#include <texteditor/textsuggestion.h>
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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#include "RefactorSuggestionHoverHandler.hpp"
#include "RefactorSuggestion.hpp"
#include <QColor>
#include <QHBoxLayout>
#include <QPushButton>
#include <QScopeGuard>
#include <QTextBlock>
#include <QTextCursor>
#include <QWidget>
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditor.h>
#include <utils/theme/theme.h>
#include <utils/tooltip/tooltip.h>
#include <logger/Logger.hpp>
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<RefactorSuggestion *>(
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<RefactorSuggestion *>(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<RefactorSuggestion *>(
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<RefactorSuggestion *>(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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <QTextBlock>
#include <texteditor/basehoverhandler.h>
#include <utils/textutils.h>
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<void()>;
using DismissCallback = std::function<void()>;
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

View File

@@ -53,7 +53,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled)
bool isToolsEnabled,
bool isThinkingEnabled)
= 0;
virtual QList<QString> getInstalledModels(const QString &url) = 0;
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;

View File

@@ -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();
}

View File

@@ -42,7 +42,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled) override;
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;

View File

@@ -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;
};
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
const auto &chatSettings = Settings::chatAssistantSettings();
if (chatSettings.enableThinkingMode()) {
auto applyThinkingMode = [&request](const auto &settings) {
QJsonObject generationConfig;
generationConfig["maxOutputTokens"] = chatSettings.thinkingMaxTokens();
generationConfig["maxOutputTokens"] = settings.thinkingMaxTokens();
if (chatSettings.useTopP())
generationConfig["topP"] = chatSettings.topP();
if (chatSettings.useTopK())
generationConfig["topK"] = chatSettings.topK();
if (settings.useTopP())
generationConfig["topP"] = settings.topP();
if (settings.useTopK())
generationConfig["topK"] = settings.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
int budgetTokens = settings.thinkingBudgetTokens();
if (budgetTokens != -1) {
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 (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 (isThinkingEnabled) {
applyThinkingMode(chatSettings);
} else {
applyModelParams(chatSettings);
}

View File

@@ -41,7 +41,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled) override;
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;

View File

@@ -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());
}

View File

@@ -41,7 +41,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled) override;
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;

View File

@@ -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 <QEventLoop>
@@ -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());
}

View File

@@ -41,7 +41,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled) override;
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;

View File

@@ -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());
}

View File

@@ -41,7 +41,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled) override;
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;

View File

@@ -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());
}

View File

@@ -42,7 +42,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled) override;
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;

View File

@@ -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());
}

View File

@@ -41,7 +41,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled) override;
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;

View File

@@ -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());
}

View File

@@ -41,7 +41,8 @@ public:
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled) override;
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;

View File

@@ -22,6 +22,7 @@
#include <utils/aspects.h>
#include <utils/layoutbuilder.h>
#include <QPushButton>
#include <QIcon>
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();

View File

@@ -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

View File

@@ -21,11 +21,12 @@
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/icore.h>
#include <utils/detailswidget.h>
#include <utils/layoutbuilder.h>
#include <utils/utilsicons.h>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QTextEdit>
#include <QTimer>
#include <QtWidgets/qboxlayout.h>
#include <QtWidgets/qcompleter.h>
@@ -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("<b>%1:</b> %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();
}
}

View File

@@ -20,9 +20,14 @@
#pragma once
#include <utils/aspects.h>
#include <QPointer>
#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:

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#include "QuickRefactorSettings.hpp"
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <QMessageBox>
#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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <utils/aspects.h>
#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

View File

@@ -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

View File

@@ -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?");

View File

@@ -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);
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#include "CompletionErrorHandler.hpp"
#include <texteditor/texteditor.h>
#include <utils/tooltip/tooltip.h>
#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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <texteditor/basehoverhandler.h>
#include <QPointer>
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<TextEditor::TextEditorWidget> m_widget;
QPointer<ErrorWidget> m_errorWidget;
QString m_errorMessage;
QPoint m_errorPosition;
};
} // namespace QodeAssist

View File

@@ -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();
m_progressWidget = new ProgressWidget(editorWidget);
Utils::ToolTip::show(showPoint, 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

View File

@@ -24,6 +24,8 @@
namespace QodeAssist {
class ProgressWidget;
class CompletionProgressHandler : public TextEditor::BaseHoverHandler
{
public:
@@ -37,6 +39,7 @@ protected:
private:
QPointer<TextEditor::TextEditorWidget> m_widget;
QPointer<ProgressWidget> m_progressWidget;
QPoint m_iconPosition;
};

213
widgets/ErrorWidget.cpp Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#include "ErrorWidget.hpp"
#include <QFontMetrics>
#include <QMouseEvent>
#include <QPainterPath>
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

65
widgets/ErrorWidget.hpp Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QPainter>
#include <QTimer>
#include <QWidget>
#include <utils/theme/theme.h>
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