TextEditor: add generic cyclic suggestion

And use the added suggestion for copilot.

Change-Id: I896fa4c74c7099e9c6df483d4f92617da910a733
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
David Schulz
2024-09-02 15:21:24 +02:00
parent 167c98cc74
commit 3f222c530c
12 changed files with 162 additions and 188 deletions

View File

@@ -9,7 +9,6 @@ add_qtc_plugin(Copilot
copilotplugin.cpp copilotplugin.cpp
copilotprojectpanel.cpp copilotprojectpanel.h copilotprojectpanel.cpp copilotprojectpanel.h
copilotsettings.cpp copilotsettings.h copilotsettings.cpp copilotsettings.h
copilotsuggestion.cpp copilotsuggestion.h
requests/checkstatus.h requests/checkstatus.h
requests/getcompletions.h requests/getcompletions.h
requests/signinconfirm.h requests/signinconfirm.h

View File

@@ -23,8 +23,6 @@ QtcPlugin {
"copilotprojectpanel.h", "copilotprojectpanel.h",
"copilotsettings.cpp", "copilotsettings.cpp",
"copilotsettings.h", "copilotsettings.h",
"copilotsuggestion.cpp",
"copilotsuggestion.h",
"requests/checkstatus.h", "requests/checkstatus.h",
"requests/getcompletions.h", "requests/getcompletions.h",
"requests/signinconfirm.h", "requests/signinconfirm.h",

View File

@@ -3,7 +3,6 @@
#include "copilotclient.h" #include "copilotclient.h"
#include "copilotsettings.h" #include "copilotsettings.h"
#include "copilotsuggestion.h"
#include "copilottr.h" #include "copilottr.h"
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
@@ -228,10 +227,19 @@ void CopilotClient::handleCompletions(const GetCompletionRequest::Response &resp
if (delta > 0) if (delta > 0)
completion.setText(completionText.chopped(delta)); completion.setText(completionText.chopped(delta));
} }
auto suggestions = Utils::transform(completions, [](const Completion &c){
auto toTextPos = [](const LanguageServerProtocol::Position pos){
return Text::Position{pos.line() + 1, pos.character()};
};
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
Text::Position pos{toTextPos(c.position())};
return CyclicSuggestion::Data{range, pos, c.text()};
});
if (completions.isEmpty()) if (completions.isEmpty())
return; return;
editor->insertSuggestion( editor->insertSuggestion(
std::make_unique<CopilotSuggestion>(completions, editor->document())); std::make_unique<TextEditor::CyclicSuggestion>(suggestions, editor->document()));
editor->addHoverHandler(&m_hoverHandler); editor->addHoverHandler(&m_hoverHandler);
} }
} }

View File

@@ -4,7 +4,6 @@
#include "copilothoverhandler.h" #include "copilothoverhandler.h"
#include "copilotclient.h" #include "copilotclient.h"
#include "copilotsuggestion.h"
#include "copilottr.h" #include "copilottr.h"
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
@@ -28,21 +27,21 @@ namespace Copilot::Internal {
class CopilotCompletionToolTip : public QToolBar class CopilotCompletionToolTip : public QToolBar
{ {
public: public:
CopilotCompletionToolTip(QList<Completion> completions, CopilotCompletionToolTip(QList<CyclicSuggestion::Data> suggestions,
int currentCompletion, int currentSuggestion,
TextEditorWidget *editor) TextEditorWidget *editor)
: m_numberLabel(new QLabel) : m_numberLabel(new QLabel)
, m_completions(completions) , m_suggestions(suggestions)
, m_currentCompletion(std::max(0, std::min<int>(currentCompletion, completions.size() - 1))) , m_currentSuggestion(std::max(0, std::min<int>(currentSuggestion, suggestions.size() - 1)))
, m_editor(editor) , m_editor(editor)
{ {
auto prev = addAction(Utils::Icons::PREV_TOOLBAR.icon(), auto prev = addAction(Utils::Icons::PREV_TOOLBAR.icon(),
Tr::tr("Select Previous Copilot Suggestion")); Tr::tr("Select Previous Copilot Suggestion"));
prev->setEnabled(m_completions.size() > 1); prev->setEnabled(m_suggestions.size() > 1);
addWidget(m_numberLabel); addWidget(m_numberLabel);
auto next = addAction(Utils::Icons::NEXT_TOOLBAR.icon(), auto next = addAction(Utils::Icons::NEXT_TOOLBAR.icon(),
Tr::tr("Select Next Copilot Suggestion")); Tr::tr("Select Next Copilot Suggestion"));
next->setEnabled(m_completions.size() > 1); next->setEnabled(m_suggestions.size() > 1);
auto apply = addAction(Tr::tr("Apply (%1)").arg(QKeySequence(Qt::Key_Tab).toString())); auto apply = addAction(Tr::tr("Apply (%1)").arg(QKeySequence(Qt::Key_Tab).toString()));
auto applyWord = addAction( auto applyWord = addAction(
@@ -62,34 +61,33 @@ private:
void updateLabels() void updateLabels()
{ {
m_numberLabel->setText(Tr::tr("%1 of %2") m_numberLabel->setText(Tr::tr("%1 of %2")
.arg(m_currentCompletion + 1) .arg(m_currentSuggestion + 1)
.arg(m_completions.count())); .arg(m_suggestions.count()));
} }
void selectPrevious() void selectPrevious()
{ {
--m_currentCompletion; --m_currentSuggestion;
if (m_currentCompletion < 0) if (m_currentSuggestion < 0)
m_currentCompletion = m_completions.size() - 1; m_currentSuggestion = m_suggestions.size() - 1;
setCurrentCompletion(); setCurrentSuggestion();
} }
void selectNext() void selectNext()
{ {
++m_currentCompletion; ++m_currentSuggestion;
if (m_currentCompletion >= m_completions.size()) if (m_currentSuggestion >= m_suggestions.size())
m_currentCompletion = 0; m_currentSuggestion = 0;
setCurrentCompletion(); setCurrentSuggestion();
} }
void setCurrentCompletion() void setCurrentSuggestion()
{ {
updateLabels(); updateLabels();
if (TextSuggestion *suggestion = m_editor->currentSuggestion()) if (TextSuggestion *suggestion = m_editor->currentSuggestion())
suggestion->reset(); suggestion->reset();
m_editor->insertSuggestion(std::make_unique<CopilotSuggestion>(m_completions, m_editor->insertSuggestion(std::make_unique<CyclicSuggestion>(
m_editor->document(), m_suggestions, m_editor->document(), m_currentSuggestion));
m_currentCompletion));
} }
void apply() void apply()
@@ -120,8 +118,8 @@ private:
} }
QLabel *m_numberLabel; QLabel *m_numberLabel;
QList<Completion> m_completions; QList<CyclicSuggestion::Data> m_suggestions;
int m_currentCompletion = 0; int m_currentSuggestion = 0;
TextEditorWidget *m_editor; TextEditorWidget *m_editor;
}; };
@@ -136,13 +134,13 @@ void CopilotHoverHandler::identifyMatch(TextEditorWidget *editorWidget,
QTextCursor cursor(editorWidget->document()); QTextCursor cursor(editorWidget->document());
cursor.setPosition(pos); cursor.setPosition(pos);
m_block = cursor.block(); m_block = cursor.block();
auto *suggestion = dynamic_cast<CopilotSuggestion *>(TextDocumentLayout::suggestion(m_block)); auto *suggestion = dynamic_cast<CyclicSuggestion *>(TextDocumentLayout::suggestion(m_block));
if (!suggestion) if (!suggestion)
return; return;
const QList<Completion> completions = suggestion->completions(); const QList<CyclicSuggestion::Data> suggestions = suggestion->suggestions();
if (completions.isEmpty()) if (suggestions.isEmpty())
return; return;
cleanup.dismiss(); cleanup.dismiss();
@@ -152,13 +150,13 @@ void CopilotHoverHandler::identifyMatch(TextEditorWidget *editorWidget,
void CopilotHoverHandler::operateTooltip(TextEditorWidget *editorWidget, const QPoint &point) void CopilotHoverHandler::operateTooltip(TextEditorWidget *editorWidget, const QPoint &point)
{ {
Q_UNUSED(point) Q_UNUSED(point)
auto *suggestion = dynamic_cast<CopilotSuggestion *>(TextDocumentLayout::suggestion(m_block)); auto *suggestion = dynamic_cast<CyclicSuggestion *>(TextDocumentLayout::suggestion(m_block));
if (!suggestion) if (!suggestion)
return; return;
auto tooltipWidget = new CopilotCompletionToolTip(suggestion->completions(), auto tooltipWidget = new CopilotCompletionToolTip(suggestion->suggestions(),
suggestion->currentCompletion(), suggestion->currentSuggestion(),
editorWidget); editorWidget);
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor()); const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());

View File

@@ -6,7 +6,6 @@
#include "copiloticons.h" #include "copiloticons.h"
#include "copilotprojectpanel.h" #include "copilotprojectpanel.h"
#include "copilotsettings.h" #include "copilotsettings.h"
#include "copilotsuggestion.h"
#include "copilottr.h" #include "copilottr.h"
#include <coreplugin/actionmanager/actionmanager.h> #include <coreplugin/actionmanager/actionmanager.h>
@@ -35,21 +34,20 @@ enum Direction { Previous, Next };
static void cycleSuggestion(TextEditor::TextEditorWidget *editor, Direction direction) static void cycleSuggestion(TextEditor::TextEditorWidget *editor, Direction direction)
{ {
QTextBlock block = editor->textCursor().block(); QTextBlock block = editor->textCursor().block();
if (auto suggestion = dynamic_cast<CopilotSuggestion *>( if (auto suggestion = dynamic_cast<TextEditor::CyclicSuggestion *>(
TextEditor::TextDocumentLayout::suggestion(block))) { TextEditor::TextDocumentLayout::suggestion(block))) {
int index = suggestion->currentCompletion(); int index = suggestion->currentSuggestion();
if (direction == Previous) if (direction == Previous)
--index; --index;
else else
++index; ++index;
if (index < 0) if (index < 0)
index = suggestion->completions().count() - 1; index = suggestion->suggestions().count() - 1;
else if (index >= suggestion->completions().count()) else if (index >= suggestion->suggestions().count())
index = 0; index = 0;
suggestion->reset(); suggestion->reset();
editor->insertSuggestion(std::make_unique<CopilotSuggestion>(suggestion->completions(), editor->insertSuggestion(std::make_unique<TextEditor::CyclicSuggestion>(
editor->document(), suggestion->suggestions(), editor->document(), index));
index));
} }
} }

View File

@@ -1,104 +0,0 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "copilotsuggestion.h"
#include <texteditor/texteditor.h>
#include <utils/stringutils.h>
using namespace Utils;
using namespace TextEditor;
using namespace LanguageServerProtocol;
namespace Copilot::Internal {
CopilotSuggestion::CopilotSuggestion(const QList<Completion> &completions,
QTextDocument *origin,
int currentCompletion)
: m_completions(completions)
, m_currentCompletion(currentCompletion)
{
const Completion completion = completions.value(currentCompletion);
const Position start = completion.range().start();
const Position end = completion.range().end();
QString text = start.toTextCursor(origin).block().text();
int length = text.length() - start.character();
if (start.line() == end.line())
length = end.character() - start.character();
text.replace(start.character(), length, completion.text());
document()->setPlainText(text);
m_start = completion.position().toTextCursor(origin);
m_start.setKeepPositionOnInsert(true);
}
bool CopilotSuggestion::apply()
{
reset();
const Completion completion = m_completions.value(m_currentCompletion);
QTextCursor cursor = completion.range().toSelection(m_start.document());
cursor.insertText(completion.text());
return true;
}
bool CopilotSuggestion::applyWord(TextEditorWidget *widget)
{
return applyPart(Word, widget);
}
bool CopilotSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
{
return applyPart(Line, widget);
}
void CopilotSuggestion::reset()
{
m_start.removeSelectedText();
}
int CopilotSuggestion::position()
{
return m_start.selectionEnd();
}
bool CopilotSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
{
Completion completion = m_completions.value(m_currentCompletion);
const Range range = completion.range();
const QTextCursor cursor = range.toSelection(m_start.document());
QTextCursor currentCursor = widget->textCursor();
const QString text = completion.text();
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
+ (cursor.selectionEnd() - cursor.selectionStart());
int next = part == Word ? endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
if (next == -1)
return apply();
if (part == Line)
++next;
QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty())
return false;
currentCursor.insertText(subText);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) {
completion.setText(newCompletionText);
const Position newStart(range.start().line() + subText.count('\n'), 0);
int nextSeperatorPos = newCompletionText.indexOf('\n');
if (nextSeperatorPos == -1)
nextSeperatorPos = newCompletionText.size();
const Position newEnd(newStart.line(), nextSeperatorPos);
completion.setRange(Range(newStart, newEnd));
completion.setPosition(newStart);
widget->insertSuggestion(std::make_unique<CopilotSuggestion>(
QList<Completion>{completion}, widget->document(), 0));
}
}
return false;
}
} // namespace Copilot::Internal

View File

@@ -1,36 +0,0 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "requests/getcompletions.h"
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditor.h>
namespace Copilot::Internal {
class CopilotSuggestion final : public TextEditor::TextSuggestion
{
public:
CopilotSuggestion(const QList<Completion> &completions,
QTextDocument *origin,
int currentCompletion = 0);
bool apply() final;
bool applyWord(TextEditor::TextEditorWidget *widget) final;
bool applyLine(TextEditor::TextEditorWidget *widget) final;
void reset() final;
int position() final;
const QList<Completion> &completions() const { return m_completions; }
int currentCompletion() const { return m_currentCompletion; }
private:
enum Part {Word, Line};
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
QList<Completion> m_completions;
int m_currentCompletion = 0;
QTextCursor m_start;
};
} // namespace Copilot::Internal

View File

@@ -119,8 +119,6 @@ public:
virtual void reset() override { m_start.removeSelectedText(); } virtual void reset() override { m_start.removeSelectedText(); }
virtual int position() override { return m_start.selectionEnd(); }
qsizetype size() const { return m_suggestions.size(); } qsizetype size() const { return m_suggestions.size(); }
bool isEmpty() const { return m_suggestions.isEmpty(); } bool isEmpty() const { return m_suggestions.isEmpty(); }

View File

@@ -407,7 +407,7 @@ QAction *TextDocument::createDiffAgainstCurrentFileAction(
void TextDocument::insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion) void TextDocument::insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion)
{ {
QTextCursor cursor(&d->m_document); QTextCursor cursor(&d->m_document);
cursor.setPosition(suggestion->position()); cursor.setPosition(suggestion->currentPosition());
const QTextBlock block = cursor.block(); const QTextBlock block = cursor.block();
TextDocumentLayout::userData(block)->insertSuggestion(std::move(suggestion)); TextDocumentLayout::userData(block)->insertSuggestion(std::move(suggestion));
TextDocumentLayout::updateSuggestionFormats(block, fontSettings()); TextDocumentLayout::updateSuggestionFormats(block, fontSettings());

View File

@@ -1823,8 +1823,7 @@ void TextEditorWidgetPrivate::insertSuggestion(std::unique_ptr<TextSuggestion> &
return; return;
auto cursor = q->textCursor(); auto cursor = q->textCursor();
suggestion->setCurrentPosition(cursor.position()); cursor.setPosition(suggestion->currentPosition());
cursor.setPosition(suggestion->position());
QTextOption option = suggestion->document()->defaultTextOption(); QTextOption option = suggestion->document()->defaultTextOption();
option.setTabStopDistance(charWidth() * m_document->tabSettings().m_tabSize); option.setTabStopDistance(charWidth() * m_document->tabSettings().m_tabSize);
suggestion->document()->setDefaultTextOption(option); suggestion->document()->setDefaultTextOption(option);

View File

@@ -4,6 +4,12 @@
#include "textsuggestion.h" #include "textsuggestion.h"
#include "textdocumentlayout.h" #include "textdocumentlayout.h"
#include "texteditor.h"
#include <utils/qtcassert.h>
#include <utils/stringutils.h>
using namespace Utils;
namespace TextEditor { namespace TextEditor {
@@ -15,4 +21,81 @@ TextSuggestion::TextSuggestion()
TextSuggestion::~TextSuggestion() = default; TextSuggestion::~TextSuggestion() = default;
CyclicSuggestion::CyclicSuggestion(const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentSuggestion)
: m_suggestions(suggestions)
, m_currentSuggestion(currentSuggestion)
, m_sourceDocument(sourceDocument)
{
if (QTC_GUARD(!suggestions.isEmpty())) {
QTC_ASSERT(
m_currentSuggestion >= 0 && m_currentSuggestion < suggestions.size(),
m_currentSuggestion = 0);
Data current = suggestions.at(m_currentSuggestion);
document()->setPlainText(current.text);
setCurrentPosition(current.position.toPositionInDocument(sourceDocument));
}
}
bool CyclicSuggestion::apply()
{
const Data &suggestion = m_suggestions.value(m_currentSuggestion);
QTextCursor c = suggestion.range.begin.toTextCursor(m_sourceDocument);
c.setPosition(currentPosition(), QTextCursor::KeepAnchor);
c.insertText(suggestion.text);
return true;
}
bool CyclicSuggestion::applyWord(TextEditorWidget *widget)
{
return applyPart(Word, widget);
}
bool CyclicSuggestion::applyLine(TextEditorWidget *widget)
{
return applyPart(Line, widget);
}
void CyclicSuggestion::reset()
{
const Data &suggestion = m_suggestions.value(m_currentSuggestion);
QTextCursor c = suggestion.position.toTextCursor(m_sourceDocument);
c.setPosition(currentPosition(), QTextCursor::KeepAnchor);
c.removeSelectedText();
}
bool CyclicSuggestion::applyPart(Part part, TextEditorWidget *widget)
{
const Data suggestion = m_suggestions.value(m_currentSuggestion);
const Text::Range range = suggestion.range;
const QTextCursor cursor = range.toTextCursor(m_sourceDocument);
QTextCursor currentCursor = widget->textCursor();
const QString text = suggestion.text;
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
+ (cursor.selectionEnd() - cursor.selectionStart());
int next = part == Word ? endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
if (next == -1)
return apply();
if (part == Line)
++next;
QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty())
return false;
currentCursor.insertText(subText);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) {
const Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
const Text::Position newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
const Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<CyclicSuggestion>(newSuggestion, widget->document(), 0));
}
}
return false;
}
} // namespace TextEditor } // namespace TextEditor

View File

@@ -5,6 +5,9 @@
#include "texteditor_global.h" #include "texteditor_global.h"
#include <utils/textutils.h>
#include <QTextCursor>
#include <QTextDocument> #include <QTextDocument>
namespace TextEditor { namespace TextEditor {
@@ -22,7 +25,6 @@ public:
virtual bool applyWord(TextEditorWidget *widget) = 0; virtual bool applyWord(TextEditorWidget *widget) = 0;
virtual bool applyLine(TextEditorWidget *widget) = 0; virtual bool applyLine(TextEditorWidget *widget) = 0;
virtual void reset() = 0; virtual void reset() = 0;
virtual int position() = 0;
int currentPosition() const { return m_currentPosition; } int currentPosition() const { return m_currentPosition; }
void setCurrentPosition(int position) { m_currentPosition = position; } void setCurrentPosition(int position) { m_currentPosition = position; }
@@ -34,4 +36,35 @@ private:
int m_currentPosition = -1; int m_currentPosition = -1;
}; };
class TEXTEDITOR_EXPORT CyclicSuggestion : public TextSuggestion
{
public:
class TEXTEDITOR_EXPORT Data
{
public:
Utils::Text::Range range;
Utils::Text::Position position;
QString text;
};
CyclicSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
bool apply() override;
bool applyWord(TextEditorWidget *widget) override;
bool applyLine(TextEditorWidget *widget) override;
void reset() override;
QList<Data> suggestions() const { return m_suggestions; }
int currentSuggestion() const { return m_currentSuggestion; }
private:
enum Part {Word, Line};
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
QList<Data> m_suggestions;
int m_currentSuggestion = 0;
QTextDocument *m_sourceDocument = nullptr;
};
} // namespace TextEditor } // namespace TextEditor