♻️ refactor: Multiline text suggestion

This commit is contained in:
Petr Mironychev 2024-12-17 00:47:15 +01:00
parent ac8080542d
commit 09cde8fd3d
3 changed files with 67 additions and 85 deletions

View File

@ -1,8 +1,13 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify * QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@ -18,27 +23,25 @@
*/ */
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include <QTextCursor>
#include <QtWidgets/qtoolbar.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <utils/stringutils.h> #include <utils/stringutils.h>
#include <utils/tooltip/tooltip.h> #include <utils/tooltip/tooltip.h>
namespace QodeAssist { namespace QodeAssist {
LLMSuggestion::LLMSuggestion(const TextEditor::TextSuggestion::Data &data, QTextDocument *origin) LLMSuggestion::LLMSuggestion(
: TextEditor::TextSuggestion(data, origin) const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
, m_linesCount(0) : TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
, m_suggestion(data)
{ {
int startPos = data.range.begin.toPositionInDocument(origin); const auto &data = suggestions[currentCompletion];
int endPos = data.range.end.toPositionInDocument(origin);
startPos = qBound(0, startPos, origin->characterCount() - 1); int startPos = data.range.begin.toPositionInDocument(sourceDocument);
endPos = qBound(startPos, endPos, origin->characterCount() - 1); int endPos = data.range.end.toPositionInDocument(sourceDocument);
QTextCursor cursor(origin); startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
QTextCursor cursor(sourceDocument);
cursor.setPosition(startPos); cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor); cursor.setPosition(endPos, QTextCursor::KeepAnchor);
@ -49,64 +52,56 @@ LLMSuggestion::LLMSuggestion(const TextEditor::TextSuggestion::Data &data, QText
int endPosInBlock = endPos - block.position(); int endPosInBlock = endPos - block.position();
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text); blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
replacementDocument()->setPlainText(blockText); replacementDocument()->setPlainText(blockText);
} }
bool LLMSuggestion::apply()
{
return TextEditor::TextSuggestion::apply();
}
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
{ {
return TextEditor::TextSuggestion::applyWord(widget); return applyPart(Word, widget);
} }
bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
{ {
return TextEditor::TextSuggestion::applyLine(widget); return applyPart(Line, widget);
} }
void LLMSuggestion::reset() bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
{ {
reset(); const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
m_linesCount = 0; const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
} QTextCursor currentCursor = widget->textCursor();
const QString text = suggestions()[currentSuggestion()].text;
void LLMSuggestion::onCounterFinished(int count) const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
{ + (cursor.selectionEnd() - cursor.selectionStart());
Utils::ToolTip::hide();
m_linesCount = 0;
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
cursor.beginEditBlock();
cursor.removeSelectedText();
QStringList lines = m_completion.text().split('\n'); int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
QString textToInsert = lines.mid(0, count).join('\n');
cursor.insertText(textToInsert); if (next == -1)
cursor.endEditBlock(); return apply();
}
// void LLMSuggestion::reset() if (part == Line)
// { ++next;
// m_start.removeSelectedText();
// }
// int LLMSuggestion::position() QString subText = text.mid(startPos, next - startPos);
// { if (subText.isEmpty())
// return m_start.position(); return false;
// }
void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count) currentCursor.insertText(subText);
{
// Utils::ToolTip::hide(); if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
// QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight()); const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
// pos += QPoint(-10, -50); if (!newCompletionText.isEmpty()) {
// m_counterTooltip = new CounterTooltip(count); const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
// Utils::ToolTip::show(pos, m_counterTooltip, widget); const Utils::Text::Position
// connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished); newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
}
}
return false;
} }
} // namespace QodeAssist } // namespace QodeAssist

View File

@ -1,8 +1,13 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify * QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@ -19,36 +24,21 @@
#pragma once #pragma once
#include <QObject> #include <texteditor/texteditor.h>
#include "LSPCompletion.hpp" #include <texteditor/textsuggestion.h>
#include <texteditor/textdocumentlayout.h>
#include "utils/CounterTooltip.hpp"
namespace QodeAssist { namespace QodeAssist {
class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion class LLMSuggestion : public TextEditor::CyclicSuggestion
{ {
Q_OBJECT
public: public:
LLMSuggestion(const TextEditor::TextSuggestion::Data &data, QTextDocument *origin); enum Part { Word, Line };
LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
bool apply() override;
bool applyWord(TextEditor::TextEditorWidget *widget) override; bool applyWord(TextEditor::TextEditorWidget *widget) override;
bool applyLine(TextEditor::TextEditorWidget *widget) override; bool applyLine(TextEditor::TextEditorWidget *widget) override;
void reset(); bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
void showTooltip(TextEditor::TextEditorWidget *widget, int count);
void onCounterFinished(int count);
private:
Completion m_completion;
QTextCursor m_start;
int m_linesCount;
CounterTooltip *m_counterTooltip = nullptr;
int m_startPosition;
TextEditor::TextSuggestion::Data m_suggestion;
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@ -193,8 +193,8 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
auto isValidCompletion = [](const Completion &completion) { auto isValidCompletion = [](const Completion &completion) {
return completion.isValid() && !completion.text().trimmed().isEmpty(); return completion.isValid() && !completion.text().trimmed().isEmpty();
}; };
QList<Completion> completions = Utils::filtered(result->completions().toListOrEmpty(), QList<Completion> completions
isValidCompletion); = Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
// remove trailing whitespaces from the end of the completions // remove trailing whitespaces from the end of the completions
for (Completion &completion : completions) { for (Completion &completion : completions) {
@ -211,9 +211,6 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
if (delta > 0) if (delta > 0)
completion.setText(completionText.chopped(delta)); completion.setText(completionText.chopped(delta));
} }
if (completions.isEmpty())
return;
auto suggestions = Utils::transform(completions, [](const Completion &c) { auto suggestions = Utils::transform(completions, [](const Completion &c) {
auto toTextPos = [](const LanguageServerProtocol::Position pos) { auto toTextPos = [](const LanguageServerProtocol::Position pos) {
return Text::Position{pos.line() + 1, pos.character()}; return Text::Position{pos.line() + 1, pos.character()};
@ -223,9 +220,9 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
Text::Position pos{toTextPos(c.position())}; Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()}; return TextSuggestion::Data{range, pos, c.text()};
}); });
if (completions.isEmpty())
editor->insertSuggestion( return;
std::make_unique<LLMSuggestion>(suggestions.first(), editor->document())); editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
} }
} }