Copilot: add copilot suggestion tooltips

These tooltips allow to switch the currently visible suggestion as well
as applying it using
the mouse.

Change-Id: I30b9a76ae57c66887f4e1b1311e1a7248ed0f194
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
David Schulz
2023-03-16 06:38:46 +01:00
parent 8a1e34f084
commit 6ab923c39f
13 changed files with 303 additions and 56 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -3,13 +3,15 @@ add_qtc_plugin(Copilot
SOURCES SOURCES
authwidget.cpp authwidget.h authwidget.cpp authwidget.h
copilot.qrc copilot.qrc
copilotplugin.cpp copilotplugin.h
copilotclient.cpp copilotclient.h copilotclient.cpp copilotclient.h
copilotsettings.cpp copilotsettings.h copilothoverhandler.cpp copilothoverhandler.h
copilotoptionspage.cpp copilotoptionspage.h copilotoptionspage.cpp copilotoptionspage.h
requests/getcompletions.h copilotplugin.cpp copilotplugin.h
copilotsettings.cpp copilotsettings.h
copilotsuggestion.cpp copilotsuggestion.h
requests/checkstatus.h requests/checkstatus.h
requests/signout.h requests/getcompletions.h
requests/signininitiate.h
requests/signinconfirm.h requests/signinconfirm.h
requests/signininitiate.h
requests/signout.h
) )

View File

@@ -12,18 +12,22 @@ QtcPlugin {
"authwidget.cpp", "authwidget.cpp",
"authwidget.h", "authwidget.h",
"copilot.qrc", "copilot.qrc",
"copilotplugin.cpp",
"copilotplugin.h",
"copilotclient.cpp", "copilotclient.cpp",
"copilotclient.h", "copilotclient.h",
"copilotsettings.cpp", "copilothoverhandler.cpp",
"copilotsettings.h", "copilothoverhandler.h",
"copilotoptionspage.cpp", "copilotoptionspage.cpp",
"copilotoptionspage.h", "copilotoptionspage.h",
"requests/getcompletions.h", "copilotplugin.cpp",
"copilotplugin.h",
"copilotsettings.cpp",
"copilotsettings.h",
"copilotsuggestion.cpp",
"copilotsuggestion.h",
"requests/checkstatus.h", "requests/checkstatus.h",
"requests/signout.h", "requests/getcompletions.h",
"requests/signininitiate.h",
"requests/signinconfirm.h", "requests/signinconfirm.h",
"requests/signininitiate.h",
"requests/signout.h",
] ]
} }

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "copilotclient.h" #include "copilotclient.h"
#include "copilotsuggestion.h"
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <languageclient/languageclientmanager.h> #include <languageclient/languageclientmanager.h>
@@ -63,6 +64,14 @@ CopilotClient::CopilotClient(const FilePath &nodePath, const FilePath &distPath)
openDoc(doc); openDoc(doc);
} }
CopilotClient::~CopilotClient()
{
for (Core::IEditor *editor : Core::DocumentModel::editorsForOpenedDocuments()) {
if (auto textEditor = qobject_cast<BaseTextEditor *>(editor))
textEditor->editorWidget()->removeHoverHandler(&m_hoverHandler);
}
}
void CopilotClient::openDocument(TextDocument *document) void CopilotClient::openDocument(TextDocument *document)
{ {
Client::openDocument(document); Client::openDocument(document);
@@ -112,7 +121,7 @@ void CopilotClient::scheduleRequest(TextEditorWidget *editor)
void CopilotClient::requestCompletions(TextEditorWidget *editor) void CopilotClient::requestCompletions(TextEditorWidget *editor)
{ {
Utils::MultiTextCursor cursor = editor->multiTextCursor(); Utils::MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection()) if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return; return;
const Utils::FilePath filePath = editor->textDocument()->filePath(); const Utils::FilePath filePath = editor->textDocument()->filePath();
@@ -129,39 +138,6 @@ void CopilotClient::requestCompletions(TextEditorWidget *editor)
sendMessage(request); sendMessage(request);
} }
class CopilotSuggestion final : public TextEditor::TextSuggestion
{
public:
CopilotSuggestion(const Completion &completion, QTextDocument *origin)
: m_completion(completion)
{
document()->setPlainText(completion.text());
m_start = completion.position().toTextCursor(origin);
m_start.setKeepPositionOnInsert(true);
setCurrentPosition(m_start.position());
}
bool apply() final
{
reset();
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
cursor.insertText(m_completion.text());
return true;
}
void reset() final
{
m_start.removeSelectedText();
}
int position() final
{
return m_start.position();
}
private:
Completion m_completion;
QTextCursor m_start;
};
void CopilotClient::handleCompletions(const GetCompletionRequest::Response &response, void CopilotClient::handleCompletions(const GetCompletionRequest::Response &response,
TextEditorWidget *editor) TextEditorWidget *editor)
{ {
@@ -184,7 +160,9 @@ void CopilotClient::handleCompletions(const GetCompletionRequest::Response &resp
if (completions.isEmpty()) if (completions.isEmpty())
return; return;
editor->insertSuggestion( editor->insertSuggestion(
std::make_unique<CopilotSuggestion>(completions.first(), editor->document())); std::make_unique<CopilotSuggestion>(completions, editor->document()));
m_lastCompletions[editor] = *result;
editor->addHoverHandler(&m_hoverHandler);
} }
} }
@@ -234,4 +212,9 @@ void CopilotClient::requestSignInConfirm(
sendMessage(request); sendMessage(request);
} }
GetCompletionResponse CopilotClient::lastCompletion(TextEditor::TextEditorWidget *editor) const
{
return m_lastCompletions.value(editor);
}
} // namespace Copilot::Internal } // namespace Copilot::Internal

View File

@@ -3,6 +3,7 @@
#pragma once #pragma once
#include "copilothoverhandler.h"
#include "requests/checkstatus.h" #include "requests/checkstatus.h"
#include "requests/getcompletions.h" #include "requests/getcompletions.h"
#include "requests/signinconfirm.h" #include "requests/signinconfirm.h"
@@ -18,12 +19,11 @@
namespace Copilot::Internal { namespace Copilot::Internal {
class DocumentWatcher;
class CopilotClient : public LanguageClient::Client class CopilotClient : public LanguageClient::Client
{ {
public: public:
explicit CopilotClient(const Utils::FilePath &nodePath, const Utils::FilePath &distPath); CopilotClient(const Utils::FilePath &nodePath, const Utils::FilePath &distPath);
~CopilotClient() override;
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
@@ -46,6 +46,8 @@ public:
const QString &userCode, const QString &userCode,
std::function<void(const SignInConfirmRequest::Response &response)> callback); std::function<void(const SignInConfirmRequest::Response &response)> callback);
GetCompletionResponse lastCompletion(TextEditor::TextEditorWidget *editor) const;
private: private:
QMap<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests; QMap<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
struct ScheduleData struct ScheduleData
@@ -54,6 +56,8 @@ private:
QTimer *timer = nullptr; QTimer *timer = nullptr;
}; };
QMap<TextEditor::TextEditorWidget *, ScheduleData> m_scheduledRequests; QMap<TextEditor::TextEditorWidget *, ScheduleData> m_scheduledRequests;
CopilotHoverHandler m_hoverHandler;
QHash<TextEditor::TextEditorWidget *, GetCompletionResponse> m_lastCompletions;
}; };
} // namespace Copilot::Internal } // namespace Copilot::Internal

View File

@@ -0,0 +1,140 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "copilothoverhandler.h"
#include "copilotclient.h"
#include "copilotsuggestion.h"
#include "copilottr.h"
#include <texteditor/textdocument.h>
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditor.h>
#include <utils/tooltip/tooltip.h>
#include <utils/utilsicons.h>
#include <QPushButton>
#include <QToolBar>
#include <QToolButton>
using namespace TextEditor;
using namespace LanguageServerProtocol;
using namespace Utils;
namespace Copilot::Internal {
class CopilotCompletionToolTip : public QToolBar
{
public:
CopilotCompletionToolTip(QList<Completion> completions,
int currentCompletion,
TextEditorWidget *editor)
: m_numberLabel(new QLabel)
, m_completions(completions)
, m_currentCompletion(std::max(0, std::min<int>(currentCompletion, completions.size() - 1)))
, m_editor(editor)
{
auto prev = addAction(Utils::Icons::PREV_TOOLBAR.icon(),
Tr::tr("Select Previous Copilot Suggestion"));
prev->setEnabled(m_completions.size() > 1);
addWidget(m_numberLabel);
auto next = addAction(Utils::Icons::NEXT_TOOLBAR.icon(),
Tr::tr("Select Next Copilot Suggestion"));
next->setEnabled(m_completions.size() > 1);
auto apply = addAction(Tr::tr("Apply (Tab)"));
connect(prev, &QAction::triggered, this, &CopilotCompletionToolTip::selectPrevious);
connect(next, &QAction::triggered, this, &CopilotCompletionToolTip::selectNext);
connect(apply, &QAction::triggered, this, &CopilotCompletionToolTip::apply);
updateLabels();
}
private:
void updateLabels()
{
m_numberLabel->setText(Tr::tr("%1 of %2")
.arg(m_currentCompletion + 1)
.arg(m_completions.count()));
}
void selectPrevious()
{
--m_currentCompletion;
if (m_currentCompletion < 0)
m_currentCompletion = m_completions.size() - 1;
setCurrentCompletion();
}
void selectNext()
{
++m_currentCompletion;
if (m_currentCompletion >= m_completions.size())
m_currentCompletion = 0;
setCurrentCompletion();
}
void setCurrentCompletion()
{
updateLabels();
if (TextSuggestion *suggestion = m_editor->currentSuggestion())
suggestion->reset();
m_editor->insertSuggestion(std::make_unique<CopilotSuggestion>(m_completions,
m_editor->document(),
m_currentCompletion));
}
void apply()
{
if (TextSuggestion *suggestion = m_editor->currentSuggestion())
suggestion->apply();
ToolTip::hide();
}
QLabel *m_numberLabel;
QList<Completion> m_completions;
int m_currentCompletion = 0;
TextEditorWidget *m_editor;
};
void CopilotHoverHandler::identifyMatch(TextEditorWidget *editorWidget,
int pos,
ReportPriority report)
{
auto reportNone = qScopeGuard([&] { report(Priority_None); });
if (!editorWidget->suggestionVisible())
return;
QTextCursor cursor(editorWidget->document());
cursor.setPosition(pos);
m_block = cursor.block();
auto *suggestion = dynamic_cast<CopilotSuggestion *>(TextDocumentLayout::suggestion(m_block));
if (!suggestion)
return;
const QList<Completion> completions = suggestion->completions();
if (completions.isEmpty())
return;
reportNone.dismiss();
report(Priority_Suggestion);
}
void CopilotHoverHandler::operateTooltip(TextEditorWidget *editorWidget, const QPoint &point)
{
auto *suggestion = dynamic_cast<CopilotSuggestion *>(TextDocumentLayout::suggestion(m_block));
if (!suggestion)
return;
auto tooltipWidget = new CopilotCompletionToolTip(suggestion->completions(),
suggestion->currentCompletion(),
editorWidget);
const qreal deltay = 2 * editorWidget->textDocument()->fontSettings().lineSpacing();
ToolTip::show(point - QPoint{0, int(deltay)}, tooltipWidget, editorWidget);
}
} // namespace Copilot::Internal

View File

@@ -0,0 +1,32 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "requests/getcompletions.h"
#include <texteditor/basehoverhandler.h>
#include <QTextBlock>
#pragma once
namespace TextEditor { class TextSuggestion; }
namespace Copilot::Internal {
class CopilotClient;
class CopilotHoverHandler final : public TextEditor::BaseHoverHandler
{
public:
CopilotHoverHandler() = default;
protected:
void identifyMatch(TextEditor::TextEditorWidget *editorWidget,
int pos,
ReportPriority report) final;
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) final;
private:
QTextBlock m_block;
};
} // namespace Copilot::Internal

View File

@@ -0,0 +1,41 @@
// 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"
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);
document()->setPlainText(completion.text());
m_start = completion.position().toTextCursor(origin);
m_start.setKeepPositionOnInsert(true);
setCurrentPosition(m_start.position());
}
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;
}
void CopilotSuggestion::reset()
{
m_start.removeSelectedText();
}
int CopilotSuggestion::position()
{
return m_start.position();
}
} // namespace Copilot::Internal

View File

@@ -0,0 +1,30 @@
// 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>
namespace Copilot::Internal {
class CopilotSuggestion final : public TextEditor::TextSuggestion
{
public:
CopilotSuggestion(const QList<Completion> &completions,
QTextDocument *origin,
int currentCompletion = 0);
bool apply() final;
void reset() final;
int position() final;
const QList<Completion> &completions() const { return m_completions; }
const int currentCompletion() const { return m_currentCompletion; }
private:
QList<Completion> m_completions;
int m_currentCompletion = 0;
QTextCursor m_start;
};
} // namespace Copilot::Internal

View File

@@ -39,7 +39,8 @@ protected:
Priority_None = 0, Priority_None = 0,
Priority_Tooltip = 5, Priority_Tooltip = 5,
Priority_Help = 10, Priority_Help = 10,
Priority_Diagnostic = 20 Priority_Diagnostic = 20,
Priority_Suggestion = 40
}; };
void setPriority(int priority); void setPriority(int priority);
int priority() const; int priority() const;

View File

@@ -165,7 +165,6 @@ private:
std::unique_ptr<TextSuggestion> m_suggestion; std::unique_ptr<TextSuggestion> m_suggestion;
}; };
class TEXTEDITOR_EXPORT TextDocumentLayout : public QPlainTextDocumentLayout class TEXTEDITOR_EXPORT TextDocumentLayout : public QPlainTextDocumentLayout
{ {
Q_OBJECT Q_OBJECT

View File

@@ -3736,7 +3736,10 @@ bool TextEditorWidget::viewportEvent(QEvent *event)
// Only handle tool tip for text cursor if mouse is within the block for the text cursor, // Only handle tool tip for text cursor if mouse is within the block for the text cursor,
// and not if the mouse is e.g. in the empty space behind a short line. // and not if the mouse is e.g. in the empty space behind a short line.
if (line.isValid()) { if (line.isValid()) {
if (pos.x() <= blockBoundingGeometry(block).left() + line.naturalTextRect().right()) { const QRectF blockGeometry = blockBoundingGeometry(block);
const int width = block == d->m_suggestionBlock ? blockGeometry.width()
: line.naturalTextRect().right();
if (pos.x() <= blockGeometry.left() + width) {
d->processTooltipRequest(tc); d->processTooltipRequest(tc);
return true; return true;
} else if (d->processAnnotaionTooltipRequest(block, pos)) { } else if (d->processAnnotaionTooltipRequest(block, pos)) {
@@ -5980,8 +5983,8 @@ void TextEditorWidget::addHoverHandler(BaseHoverHandler *handler)
void TextEditorWidget::removeHoverHandler(BaseHoverHandler *handler) void TextEditorWidget::removeHoverHandler(BaseHoverHandler *handler)
{ {
d->m_hoverHandlers.removeAll(handler); if (d->m_hoverHandlers.removeAll(handler) > 0)
d->m_hoverHandlerRunner.handlerRemoved(handler); d->m_hoverHandlerRunner.handlerRemoved(handler);
} }
void TextEditorWidget::insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion) void TextEditorWidget::insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion)
@@ -5994,9 +5997,16 @@ void TextEditorWidget::clearSuggestion()
d->clearCurrentSuggestion(); d->clearCurrentSuggestion();
} }
TextSuggestion *TextEditorWidget::currentSuggestion() const
{
if (d->m_suggestionBlock.isValid())
return TextDocumentLayout::suggestion(d->m_suggestionBlock);
return nullptr;
}
bool TextEditorWidget::suggestionVisible() const bool TextEditorWidget::suggestionVisible() const
{ {
return d->m_suggestionBlock.isValid(); return currentSuggestion();
} }
#ifdef WITH_TESTS #ifdef WITH_TESTS

View File

@@ -473,6 +473,7 @@ public:
void insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion); void insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion);
void clearSuggestion(); void clearSuggestion();
TextSuggestion *currentSuggestion() const;
bool suggestionVisible() const; bool suggestionVisible() const;
#ifdef WITH_TESTS #ifdef WITH_TESTS