mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-11-15 22:42:50 -05:00
416 lines
14 KiB
C++
416 lines
14 KiB
C++
/*
|
|
* 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 "QuickRefactorHandler.hpp"
|
|
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QUuid>
|
|
|
|
#include <context/DocumentContextReader.hpp>
|
|
#include <context/DocumentReaderQtCreator.hpp>
|
|
#include <context/Utils.hpp>
|
|
#include <llmcore/PromptTemplateManager.hpp>
|
|
#include <llmcore/ProvidersManager.hpp>
|
|
#include <llmcore/RequestConfig.hpp>
|
|
#include <llmcore/RulesLoader.hpp>
|
|
#include <logger/Logger.hpp>
|
|
#include <settings/ChatAssistantSettings.hpp>
|
|
#include <settings/GeneralSettings.hpp>
|
|
#include <settings/QuickRefactorSettings.hpp>
|
|
|
|
namespace QodeAssist {
|
|
|
|
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
|
: QObject(parent)
|
|
, m_currentEditor(nullptr)
|
|
, m_isRefactoringInProgress(false)
|
|
, m_contextManager(this)
|
|
{
|
|
}
|
|
|
|
QuickRefactorHandler::~QuickRefactorHandler() {}
|
|
|
|
void QuickRefactorHandler::sendRefactorRequest(
|
|
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
|
{
|
|
if (m_isRefactoringInProgress) {
|
|
cancelRequest();
|
|
}
|
|
|
|
m_currentEditor = editor;
|
|
|
|
Utils::Text::Range range;
|
|
if (editor->textCursor().hasSelection()) {
|
|
QTextCursor cursor = editor->textCursor();
|
|
int startPos = cursor.selectionStart();
|
|
int endPos = cursor.selectionEnd();
|
|
|
|
QTextBlock startBlock = editor->document()->findBlock(startPos);
|
|
int startLine = startBlock.blockNumber() + 1;
|
|
int startColumn = startPos - startBlock.position();
|
|
|
|
QTextBlock endBlock = editor->document()->findBlock(endPos);
|
|
int endLine = endBlock.blockNumber() + 1;
|
|
int endColumn = endPos - endBlock.position();
|
|
|
|
Utils::Text::Position startPosition;
|
|
startPosition.line = startLine;
|
|
startPosition.column = startColumn;
|
|
|
|
Utils::Text::Position endPosition;
|
|
endPosition.line = endLine;
|
|
endPosition.column = endColumn;
|
|
|
|
range = Utils::Text::Range();
|
|
range.begin = startPosition;
|
|
range.end = endPosition;
|
|
} else {
|
|
QTextCursor cursor = editor->textCursor();
|
|
int cursorPos = cursor.position();
|
|
|
|
QTextBlock block = editor->document()->findBlock(cursorPos);
|
|
int line = block.blockNumber() + 1;
|
|
int column = cursorPos - block.position();
|
|
|
|
Utils::Text::Position cursorPosition;
|
|
cursorPosition.line = line;
|
|
cursorPosition.column = column;
|
|
range = Utils::Text::Range();
|
|
range.begin = cursorPosition;
|
|
range.end = cursorPosition;
|
|
}
|
|
|
|
m_currentRange = range;
|
|
prepareAndSendRequest(editor, instructions, range);
|
|
}
|
|
|
|
void QuickRefactorHandler::prepareAndSendRequest(
|
|
TextEditor::TextEditorWidget *editor,
|
|
const QString &instructions,
|
|
const Utils::Text::Range &range)
|
|
{
|
|
auto &settings = Settings::generalSettings();
|
|
|
|
auto &providerRegistry = LLMCore::ProvidersManager::instance();
|
|
auto &promptManager = LLMCore::PromptTemplateManager::instance();
|
|
|
|
const auto providerName = settings.qrProvider();
|
|
auto provider = providerRegistry.getProviderByName(providerName);
|
|
|
|
if (!provider) {
|
|
QString error = QString("No provider found with name: %1").arg(providerName);
|
|
LOG_MESSAGE(error);
|
|
RefactorResult result;
|
|
result.success = false;
|
|
result.errorMessage = error;
|
|
result.editor = editor;
|
|
emit refactoringCompleted(result);
|
|
return;
|
|
}
|
|
|
|
const auto templateName = settings.qrTemplate();
|
|
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
|
|
|
|
if (!promptTemplate) {
|
|
QString error = QString("No template found with name: %1").arg(templateName);
|
|
LOG_MESSAGE(error);
|
|
RefactorResult result;
|
|
result.success = false;
|
|
result.errorMessage = error;
|
|
result.editor = editor;
|
|
emit refactoringCompleted(result);
|
|
return;
|
|
}
|
|
|
|
LLMCore::LLMConfig config;
|
|
config.requestType = LLMCore::RequestType::QuickRefactoring;
|
|
config.provider = provider;
|
|
config.promptTemplate = promptTemplate;
|
|
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,
|
|
enableTools,
|
|
enableThinking);
|
|
|
|
QString requestId = QUuid::createUuid().toString();
|
|
m_lastRequestId = requestId;
|
|
QJsonObject request{{"id", requestId}};
|
|
|
|
m_isRefactoringInProgress = true;
|
|
|
|
m_activeRequests[requestId] = {request, provider};
|
|
|
|
connect(
|
|
provider,
|
|
&LLMCore::Provider::fullResponseReceived,
|
|
this,
|
|
&QuickRefactorHandler::handleFullResponse,
|
|
Qt::UniqueConnection);
|
|
|
|
connect(
|
|
provider,
|
|
&LLMCore::Provider::requestFailed,
|
|
this,
|
|
&QuickRefactorHandler::handleRequestFailed,
|
|
Qt::UniqueConnection);
|
|
|
|
provider->sendRequest(requestId, config.url, config.providerRequest);
|
|
}
|
|
|
|
LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|
TextEditor::TextEditorWidget *editor,
|
|
const Utils::Text::Range &range,
|
|
const QString &instructions)
|
|
{
|
|
LLMCore::ContextData context;
|
|
|
|
auto textDocument = editor->textDocument();
|
|
Context::DocumentReaderQtCreator documentReader;
|
|
auto documentInfo = documentReader.readDocument(textDocument->filePath().toUrlishString());
|
|
|
|
if (!documentInfo.document) {
|
|
LOG_MESSAGE("Error: Document is not available");
|
|
return context;
|
|
}
|
|
|
|
QTextCursor cursor = editor->textCursor();
|
|
int cursorPos = cursor.position();
|
|
|
|
Context::DocumentContextReader
|
|
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
|
|
|
|
QString taggedContent;
|
|
bool readFullFile = Settings::quickRefactorSettings().readFullFile();
|
|
|
|
if (cursor.hasSelection()) {
|
|
int selStart = cursor.selectionStart();
|
|
int selEnd = cursor.selectionEnd();
|
|
|
|
QTextBlock startBlock = documentInfo.document->findBlock(selStart);
|
|
int startLine = startBlock.blockNumber();
|
|
int startColumn = selStart - startBlock.position();
|
|
|
|
QTextBlock endBlock = documentInfo.document->findBlock(selEnd);
|
|
int endLine = endBlock.blockNumber();
|
|
int endColumn = selEnd - endBlock.position();
|
|
|
|
QString contextBefore;
|
|
if (readFullFile) {
|
|
contextBefore = reader.readWholeFileBefore(startLine, startColumn);
|
|
} else {
|
|
contextBefore = reader.getContextBefore(
|
|
startLine, startColumn, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1);
|
|
}
|
|
|
|
QString selectedText = cursor.selectedText();
|
|
selectedText.replace(QChar(0x2029), "\n");
|
|
|
|
QString contextAfter;
|
|
if (readFullFile) {
|
|
contextAfter = reader.readWholeFileAfter(endLine, endColumn);
|
|
} else {
|
|
contextAfter = reader.getContextAfter(
|
|
endLine, endColumn, Settings::quickRefactorSettings().readStringsAfterCursor() + 1);
|
|
}
|
|
|
|
taggedContent = contextBefore;
|
|
if (selStart == cursorPos) {
|
|
taggedContent += "<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) {
|
|
QString projectRules = LLMCore::RulesLoader::loadRulesForProject(
|
|
project, LLMCore::RulesContext::QuickRefactor);
|
|
|
|
if (!projectRules.isEmpty()) {
|
|
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
|
LOG_MESSAGE("Loaded project rules for quick refactor");
|
|
}
|
|
}
|
|
|
|
systemPrompt += "\n\nFile information:";
|
|
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
|
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
|
|
|
systemPrompt += "\n\nCode context with position markers:";
|
|
systemPrompt += taggedContent;
|
|
|
|
systemPrompt += "\n\nOutput format:";
|
|
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
|
|
"between<selection_start><selection_end> or be "
|
|
"inserted at cursor position<cursor>";
|
|
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
|
|
"code block markers";
|
|
systemPrompt += "\n- The output should be ready to insert directly into the editor";
|
|
systemPrompt += "\n- Follow the existing code style and indentation patterns";
|
|
|
|
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
|
|
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
|
}
|
|
|
|
context.systemPrompt = systemPrompt;
|
|
|
|
QVector<LLMCore::Message> messages;
|
|
messages.append(
|
|
{"user",
|
|
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
|
: instructions});
|
|
context.history = messages;
|
|
|
|
return context;
|
|
}
|
|
|
|
void QuickRefactorHandler::handleLLMResponse(
|
|
const QString &response, const QJsonObject &request, bool isComplete)
|
|
{
|
|
if (request["id"].toString() != m_lastRequestId) {
|
|
return;
|
|
}
|
|
|
|
if (isComplete) {
|
|
m_isRefactoringInProgress = false;
|
|
|
|
QString cleanedResponse = response.trimmed();
|
|
if (cleanedResponse.startsWith("```")) {
|
|
int firstNewLine = cleanedResponse.indexOf('\n');
|
|
int lastFence = cleanedResponse.lastIndexOf("```");
|
|
|
|
if (firstNewLine != -1 && lastFence > firstNewLine) {
|
|
cleanedResponse
|
|
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
|
|
} else if (lastFence != -1) {
|
|
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
|
|
}
|
|
}
|
|
|
|
RefactorResult result;
|
|
result.newText = cleanedResponse;
|
|
result.insertRange = m_currentRange;
|
|
result.success = true;
|
|
result.editor = m_currentEditor;
|
|
|
|
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
|
|
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
|
|
LOG_MESSAGE(cleanedResponse);
|
|
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
|
|
|
emit refactoringCompleted(result);
|
|
}
|
|
}
|
|
|
|
void QuickRefactorHandler::cancelRequest()
|
|
{
|
|
if (m_isRefactoringInProgress) {
|
|
auto id = m_lastRequestId;
|
|
|
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
if (it.key() == id) {
|
|
const RequestContext &ctx = it.value();
|
|
ctx.provider->cancelRequest(id);
|
|
m_activeRequests.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_isRefactoringInProgress = false;
|
|
|
|
RefactorResult result;
|
|
result.success = false;
|
|
result.errorMessage = "Refactoring request was cancelled";
|
|
emit refactoringCompleted(result);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
} // namespace QodeAssist
|