diff options
author | David Schulz <[email protected]> | 2023-03-16 06:38:46 +0100 |
---|---|---|
committer | David Schulz <[email protected]> | 2023-03-29 07:43:28 +0000 |
commit | 6ab923c39fd4ac66edb3cf708e3c789cb4a31428 (patch) | |
tree | 14bfee4a6b4fb32b94af7fddf7605536a60a96c7 /src | |
parent | 8a1e34f084f24f45d61e0011a2b6486aadc34218 (diff) |
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 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r-- | src/plugins/copilot/CMakeLists.txt | 12 | ||||
-rw-r--r-- | src/plugins/copilot/copilot.qbs | 18 | ||||
-rw-r--r-- | src/plugins/copilot/copilotclient.cpp | 53 | ||||
-rw-r--r-- | src/plugins/copilot/copilotclient.h | 10 | ||||
-rw-r--r-- | src/plugins/copilot/copilothoverhandler.cpp | 140 | ||||
-rw-r--r-- | src/plugins/copilot/copilothoverhandler.h | 32 | ||||
-rw-r--r-- | src/plugins/copilot/copilotsuggestion.cpp | 41 | ||||
-rw-r--r-- | src/plugins/copilot/copilotsuggestion.h | 30 | ||||
-rw-r--r-- | src/plugins/texteditor/basehoverhandler.h | 3 | ||||
-rw-r--r-- | src/plugins/texteditor/textdocumentlayout.h | 1 | ||||
-rw-r--r-- | src/plugins/texteditor/texteditor.cpp | 18 | ||||
-rw-r--r-- | src/plugins/texteditor/texteditor.h | 1 |
12 files changed, 303 insertions, 56 deletions
diff --git a/src/plugins/copilot/CMakeLists.txt b/src/plugins/copilot/CMakeLists.txt index 8e8526542a7..b718ec12695 100644 --- a/src/plugins/copilot/CMakeLists.txt +++ b/src/plugins/copilot/CMakeLists.txt @@ -3,13 +3,15 @@ add_qtc_plugin(Copilot SOURCES authwidget.cpp authwidget.h copilot.qrc - copilotplugin.cpp copilotplugin.h copilotclient.cpp copilotclient.h - copilotsettings.cpp copilotsettings.h + copilothoverhandler.cpp copilothoverhandler.h copilotoptionspage.cpp copilotoptionspage.h - requests/getcompletions.h + copilotplugin.cpp copilotplugin.h + copilotsettings.cpp copilotsettings.h + copilotsuggestion.cpp copilotsuggestion.h requests/checkstatus.h - requests/signout.h - requests/signininitiate.h + requests/getcompletions.h requests/signinconfirm.h + requests/signininitiate.h + requests/signout.h ) diff --git a/src/plugins/copilot/copilot.qbs b/src/plugins/copilot/copilot.qbs index aec95112ad8..51d7febbd6b 100644 --- a/src/plugins/copilot/copilot.qbs +++ b/src/plugins/copilot/copilot.qbs @@ -12,18 +12,22 @@ QtcPlugin { "authwidget.cpp", "authwidget.h", "copilot.qrc", - "copilotplugin.cpp", - "copilotplugin.h", "copilotclient.cpp", "copilotclient.h", - "copilotsettings.cpp", - "copilotsettings.h", + "copilothoverhandler.cpp", + "copilothoverhandler.h", "copilotoptionspage.cpp", "copilotoptionspage.h", - "requests/getcompletions.h", + "copilotplugin.cpp", + "copilotplugin.h", + "copilotsettings.cpp", + "copilotsettings.h", + "copilotsuggestion.cpp", + "copilotsuggestion.h", "requests/checkstatus.h", - "requests/signout.h", - "requests/signininitiate.h", + "requests/getcompletions.h", "requests/signinconfirm.h", + "requests/signininitiate.h", + "requests/signout.h", ] } diff --git a/src/plugins/copilot/copilotclient.cpp b/src/plugins/copilot/copilotclient.cpp index 27bcbacf476..5d2a6aa4b7b 100644 --- a/src/plugins/copilot/copilotclient.cpp +++ b/src/plugins/copilot/copilotclient.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "copilotclient.h" +#include "copilotsuggestion.h" #include <languageclient/languageclientinterface.h> #include <languageclient/languageclientmanager.h> @@ -63,6 +64,14 @@ CopilotClient::CopilotClient(const FilePath &nodePath, const FilePath &distPath) 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) { Client::openDocument(document); @@ -112,7 +121,7 @@ void CopilotClient::scheduleRequest(TextEditorWidget *editor) void CopilotClient::requestCompletions(TextEditorWidget *editor) { Utils::MultiTextCursor cursor = editor->multiTextCursor(); - if (cursor.hasMultipleCursors() || cursor.hasSelection()) + if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible()) return; const Utils::FilePath filePath = editor->textDocument()->filePath(); @@ -129,39 +138,6 @@ void CopilotClient::requestCompletions(TextEditorWidget *editor) 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, TextEditorWidget *editor) { @@ -184,7 +160,9 @@ void CopilotClient::handleCompletions(const GetCompletionRequest::Response &resp if (completions.isEmpty()) return; 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); } +GetCompletionResponse CopilotClient::lastCompletion(TextEditor::TextEditorWidget *editor) const +{ + return m_lastCompletions.value(editor); +} + } // namespace Copilot::Internal diff --git a/src/plugins/copilot/copilotclient.h b/src/plugins/copilot/copilotclient.h index 5111e04e1df..13f43149f71 100644 --- a/src/plugins/copilot/copilotclient.h +++ b/src/plugins/copilot/copilotclient.h @@ -3,6 +3,7 @@ #pragma once +#include "copilothoverhandler.h" #include "requests/checkstatus.h" #include "requests/getcompletions.h" #include "requests/signinconfirm.h" @@ -18,12 +19,11 @@ namespace Copilot::Internal { -class DocumentWatcher; - class CopilotClient : public LanguageClient::Client { 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; @@ -46,6 +46,8 @@ public: const QString &userCode, std::function<void(const SignInConfirmRequest::Response &response)> callback); + GetCompletionResponse lastCompletion(TextEditor::TextEditorWidget *editor) const; + private: QMap<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests; struct ScheduleData @@ -54,6 +56,8 @@ private: QTimer *timer = nullptr; }; QMap<TextEditor::TextEditorWidget *, ScheduleData> m_scheduledRequests; + CopilotHoverHandler m_hoverHandler; + QHash<TextEditor::TextEditorWidget *, GetCompletionResponse> m_lastCompletions; }; } // namespace Copilot::Internal diff --git a/src/plugins/copilot/copilothoverhandler.cpp b/src/plugins/copilot/copilothoverhandler.cpp new file mode 100644 index 00000000000..07aca2cbc7c --- /dev/null +++ b/src/plugins/copilot/copilothoverhandler.cpp @@ -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 diff --git a/src/plugins/copilot/copilothoverhandler.h b/src/plugins/copilot/copilothoverhandler.h new file mode 100644 index 00000000000..1c48e75d5b7 --- /dev/null +++ b/src/plugins/copilot/copilothoverhandler.h @@ -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 diff --git a/src/plugins/copilot/copilotsuggestion.cpp b/src/plugins/copilot/copilotsuggestion.cpp new file mode 100644 index 00000000000..96ccbbcd18a --- /dev/null +++ b/src/plugins/copilot/copilotsuggestion.cpp @@ -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 + diff --git a/src/plugins/copilot/copilotsuggestion.h b/src/plugins/copilot/copilotsuggestion.h new file mode 100644 index 00000000000..5374ab74c05 --- /dev/null +++ b/src/plugins/copilot/copilotsuggestion.h @@ -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 diff --git a/src/plugins/texteditor/basehoverhandler.h b/src/plugins/texteditor/basehoverhandler.h index 9b6d90fd89a..c24ae4a1df5 100644 --- a/src/plugins/texteditor/basehoverhandler.h +++ b/src/plugins/texteditor/basehoverhandler.h @@ -39,7 +39,8 @@ protected: Priority_None = 0, Priority_Tooltip = 5, Priority_Help = 10, - Priority_Diagnostic = 20 + Priority_Diagnostic = 20, + Priority_Suggestion = 40 }; void setPriority(int priority); int priority() const; diff --git a/src/plugins/texteditor/textdocumentlayout.h b/src/plugins/texteditor/textdocumentlayout.h index e02615b1063..d4020f721c2 100644 --- a/src/plugins/texteditor/textdocumentlayout.h +++ b/src/plugins/texteditor/textdocumentlayout.h @@ -165,7 +165,6 @@ private: std::unique_ptr<TextSuggestion> m_suggestion; }; - class TEXTEDITOR_EXPORT TextDocumentLayout : public QPlainTextDocumentLayout { Q_OBJECT diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index c5783acb919..e61262094fc 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -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, // and not if the mouse is e.g. in the empty space behind a short line. 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); return true; } else if (d->processAnnotaionTooltipRequest(block, pos)) { @@ -5980,8 +5983,8 @@ void TextEditorWidget::addHoverHandler(BaseHoverHandler *handler) void TextEditorWidget::removeHoverHandler(BaseHoverHandler *handler) { - d->m_hoverHandlers.removeAll(handler); - d->m_hoverHandlerRunner.handlerRemoved(handler); + if (d->m_hoverHandlers.removeAll(handler) > 0) + d->m_hoverHandlerRunner.handlerRemoved(handler); } void TextEditorWidget::insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion) @@ -5994,9 +5997,16 @@ void TextEditorWidget::clearSuggestion() d->clearCurrentSuggestion(); } +TextSuggestion *TextEditorWidget::currentSuggestion() const +{ + if (d->m_suggestionBlock.isValid()) + return TextDocumentLayout::suggestion(d->m_suggestionBlock); + return nullptr; +} + bool TextEditorWidget::suggestionVisible() const { - return d->m_suggestionBlock.isValid(); + return currentSuggestion(); } #ifdef WITH_TESTS diff --git a/src/plugins/texteditor/texteditor.h b/src/plugins/texteditor/texteditor.h index 8c6bbb688e3..56308d796ed 100644 --- a/src/plugins/texteditor/texteditor.h +++ b/src/plugins/texteditor/texteditor.h @@ -473,6 +473,7 @@ public: void insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion); void clearSuggestion(); + TextSuggestion *currentSuggestion() const; bool suggestionVisible() const; #ifdef WITH_TESTS |