aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDavid Schulz <[email protected]>2023-03-16 06:38:46 +0100
committerDavid Schulz <[email protected]>2023-03-29 07:43:28 +0000
commit6ab923c39fd4ac66edb3cf708e3c789cb4a31428 (patch)
tree14bfee4a6b4fb32b94af7fddf7605536a60a96c7 /src
parent8a1e34f084f24f45d61e0011a2b6486aadc34218 (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.txt12
-rw-r--r--src/plugins/copilot/copilot.qbs18
-rw-r--r--src/plugins/copilot/copilotclient.cpp53
-rw-r--r--src/plugins/copilot/copilotclient.h10
-rw-r--r--src/plugins/copilot/copilothoverhandler.cpp140
-rw-r--r--src/plugins/copilot/copilothoverhandler.h32
-rw-r--r--src/plugins/copilot/copilotsuggestion.cpp41
-rw-r--r--src/plugins/copilot/copilotsuggestion.h30
-rw-r--r--src/plugins/texteditor/basehoverhandler.h3
-rw-r--r--src/plugins/texteditor/textdocumentlayout.h1
-rw-r--r--src/plugins/texteditor/texteditor.cpp18
-rw-r--r--src/plugins/texteditor/texteditor.h1
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