Copilot: robustify request logic

Change-Id: Ifa46bc05f0bab8e3c7fc40d855a35e940f0628da
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
David Schulz
2023-03-02 13:56:11 +01:00
parent 08bacd3f19
commit 9feef11b5d
8 changed files with 128 additions and 162 deletions

View File

@@ -7,7 +7,6 @@ add_qtc_plugin(Copilot
copilotclient.cpp copilotclient.h
copilotsettings.cpp copilotsettings.h
copilotoptionspage.cpp copilotoptionspage.h
documentwatcher.cpp documentwatcher.h
requests/getcompletions.h
requests/checkstatus.h
requests/signout.h

View File

@@ -4,7 +4,6 @@
#include "copilotclient.h"
#include "copilotsettings.h"
#include "documentwatcher.h"
#include <languageclient/languageclientinterface.h>
#include <languageclient/languageclientmanager.h>
@@ -14,6 +13,14 @@
#include <utils/filepath.h>
#include <texteditor/texteditor.h>
#include <languageserverprotocol/lsptypes.h>
#include <QTimer>
using namespace LanguageServerProtocol;
using namespace TextEditor;
using namespace Utils;
namespace Copilot::Internal {
@@ -48,51 +55,119 @@ CopilotClient::CopilotClient()
setSupportedLanguage(langFilter);
start();
connect(Core::EditorManager::instance(),
&Core::EditorManager::documentOpened,
this,
[this](Core::IDocument *document) {
TextEditor::TextDocument *textDocument = qobject_cast<TextEditor::TextDocument *>(
document);
if (!textDocument)
return;
openDocument(textDocument);
m_documentWatchers.emplace(textDocument->filePath(),
std::make_unique<DocumentWatcher>(this, textDocument));
});
auto openDoc = [this](Core::IDocument *document) {
if (auto *textDocument = qobject_cast<TextDocument *>(document))
openDocument(textDocument);
};
connect(Core::EditorManager::instance(), &Core::EditorManager::documentOpened, this, openDoc);
connect(Core::EditorManager::instance(),
&Core::EditorManager::documentClosed,
this,
[this](Core::IDocument *document) {
auto textDocument = qobject_cast<TextEditor::TextDocument *>(document);
if (!textDocument)
return;
closeDocument(textDocument);
m_documentWatchers.erase(textDocument->filePath());
if (auto textDocument = qobject_cast<TextDocument *>(document))
closeDocument(textDocument);
});
for (Core::IDocument *doc : Core::DocumentModel::openedDocuments())
openDoc(doc);
currentInstance = this;
}
void CopilotClient::requestCompletion(
const Utils::FilePath &path,
int version,
LanguageServerProtocol::Position position,
std::function<void(const GetCompletionRequest::Response &response)> callback)
void CopilotClient::openDocument(TextDocument *document)
{
GetCompletionRequest request{
{LanguageServerProtocol::TextDocumentIdentifier(hostPathToServerUri(path)),
version,
position}};
request.setResponseCallback(callback);
Client::openDocument(document);
connect(document,
&TextDocument::contentsChangedWithPosition,
this,
[this, document](int position, int charsRemoved, int charsAdded) {
auto textEditor = BaseTextEditor::currentTextEditor();
if (!textEditor || textEditor->document() != document)
return;
TextEditorWidget *widget = textEditor->editorWidget();
if (widget->multiTextCursor().hasMultipleCursors())
return;
if (widget->textCursor().position() != (position + charsAdded))
return;
scheduleRequest(textEditor->editorWidget());
});
}
void CopilotClient::scheduleRequest(TextEditorWidget *editor)
{
cancelRunningRequest(editor);
if (!m_scheduledRequests.contains(editor)) {
auto timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, [this, editor]() { requestCompletions(editor); });
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
m_scheduledRequests.remove(editor);
});
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
cancelRunningRequest(editor);
});
m_scheduledRequests.insert(editor, {editor->textCursor().position(), timer});
} else {
m_scheduledRequests[editor].cursorPosition = editor->textCursor().position();
}
m_scheduledRequests[editor].timer->start(500);
}
void CopilotClient::requestCompletions(TextEditorWidget *editor)
{
Utils::MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection())
return;
if (m_scheduledRequests[editor].cursorPosition != cursor.mainCursor().position())
return;
const Utils::FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath),
Position(cursor.mainCursor())}};
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) {
if (editor)
handleCompletions(response, editor);
});
m_runningRequests[editor] = request;
sendMessage(request);
}
void CopilotClient::handleCompletions(const GetCompletionRequest::Response &response,
TextEditorWidget *editor)
{
if (response.error())
log(*response.error());
Utils::MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection())
return;
if (const std::optional<GetCompletionResponse> result = response.result()) {
LanguageClientArray<Completion> completions = result->completions();
if (completions.isNull() || completions.toList().isEmpty())
return;
const Completion firstCompletion = completions.toList().first();
const QString content = firstCompletion.text().mid(firstCompletion.position().character());
editor->insertSuggestion(content);
}
}
void CopilotClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor)
{
auto it = m_runningRequests.find(editor);
if (it == m_runningRequests.end())
return;
cancelRequest(it->id());
m_runningRequests.erase(it);
}
void CopilotClient::requestCheckStatus(
bool localChecksOnly, std::function<void(const CheckStatusRequest::Response &response)> callback)
{

View File

@@ -27,11 +27,13 @@ public:
static CopilotClient *instance();
void requestCompletion(
const Utils::FilePath &path,
int version,
LanguageServerProtocol::Position position,
std::function<void(const GetCompletionRequest::Response &response)> callback);
void openDocument(TextEditor::TextDocument *document) override;
void scheduleRequest(TextEditor::TextEditorWidget *editor);
void requestCompletions(TextEditor::TextEditorWidget *editor);
void handleCompletions(const GetCompletionRequest::Response &response,
TextEditor::TextEditorWidget *editor);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
void requestCheckStatus(
bool localChecksOnly,
@@ -47,7 +49,13 @@ public:
std::function<void(const SignInConfirmRequest::Response &response)> callback);
private:
std::map<Utils::FilePath, std::unique_ptr<DocumentWatcher>> m_documentWatchers;
QMap<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
struct ScheduleData
{
int cursorPosition = -1;
QTimer *timer = nullptr;
};
QMap<TextEditor::TextEditorWidget *, ScheduleData> m_scheduledRequests;
};
} // namespace Copilot::Internal

View File

@@ -8,8 +8,12 @@
#include "copilotsettings.h"
#include <coreplugin/icore.h>
#include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h>
using namespace Utils;
using namespace Core;
namespace Copilot {
namespace Internal {
@@ -24,7 +28,7 @@ CopilotPlugin::~CopilotPlugin()
void CopilotPlugin::initialize()
{
CopilotSettings::instance().readSettings(Core::ICore::settings());
CopilotSettings::instance().readSettings(ICore::settings());
m_client = new CopilotClient();

View File

@@ -7,6 +7,8 @@
#include <extensionsystem/iplugin.h>
namespace TextEditor { class TextEditorWidget; }
namespace Copilot {
namespace Internal {

View File

@@ -1,90 +0,0 @@
// 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 "documentwatcher.h"
#include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h>
#include <QJsonDocument>
using namespace LanguageServerProtocol;
using namespace TextEditor;
namespace Copilot::Internal {
DocumentWatcher::DocumentWatcher(CopilotClient *client, TextDocument *textDocument)
: m_client(client)
, m_textDocument(textDocument)
{
m_lastContentSize = m_textDocument->document()->characterCount(); //toPlainText().size();
m_debounceTimer.setInterval(500);
m_debounceTimer.setSingleShot(true);
connect(textDocument, &TextDocument::contentsChanged, this, [this]() {
if (!m_isEditing) {
const int newSize = m_textDocument->document()->characterCount();
if (m_lastContentSize < newSize) {
m_debounceTimer.start();
}
m_lastContentSize = newSize;
}
});
connect(&m_debounceTimer, &QTimer::timeout, this, [this]() { getSuggestion(); });
}
void DocumentWatcher::getSuggestion()
{
TextEditorWidget *textEditorWidget = nullptr;
for (Core::IEditor *editor : Core::DocumentModel::editorsForDocument(m_textDocument)) {
textEditorWidget = qobject_cast<TextEditorWidget *>(editor->widget());
if (textEditorWidget)
break;
}
if (!textEditorWidget)
return;
Utils::MultiTextCursor cursor = textEditorWidget->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection())
return;
const int currentCursorPos = cursor.mainCursor().position();
m_client->requestCompletion(
m_textDocument->filePath(),
m_client->documentVersion(m_textDocument->filePath()),
Position(cursor.mainCursor()),
[this, textEditorWidget, currentCursorPos](const GetCompletionRequest::Response &response) {
if (response.error()) {
qDebug() << "ERROR:" << *response.error();
return;
}
const std::optional<GetCompletionResponse> result = response.result();
QTC_ASSERT(result, return);
const QList<Completion> list = result->completions().toList();
if (list.isEmpty())
return;
Utils::MultiTextCursor cursor = textEditorWidget->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection())
return;
if (cursor.cursors().first().position() != currentCursorPos)
return;
const Completion firstCompletion = list.first();
const QString content = firstCompletion.text().mid(
firstCompletion.position().character());
m_isEditing = true;
textEditorWidget->insertSuggestion(content);
m_isEditing = false;
});
}
} // namespace Copilot::Internal

View File

@@ -1,31 +0,0 @@
// 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
#pragma once
#include "copilotclient.h"
#include <texteditor/textdocument.h>
#include <QTimer>
namespace Copilot::Internal {
class DocumentWatcher : public QObject
{
Q_OBJECT
public:
explicit DocumentWatcher(CopilotClient *client, TextEditor::TextDocument *textDocument);
void getSuggestion();
private:
CopilotClient *m_client;
TextEditor::TextDocument *m_textDocument;
QTimer m_debounceTimer;
bool m_isEditing = false;
int m_lastContentSize = 0;
};
} // namespace Copilot::Internal

View File

@@ -42,7 +42,6 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
public:
static constexpr char16_t docKey[] = u"doc";
GetCompletionParams();
GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
int version,
const LanguageServerProtocol::Position &position)
@@ -110,7 +109,7 @@ class GetCompletionRequest
: public LanguageServerProtocol::Request<GetCompletionResponse, std::nullptr_t, GetCompletionParams>
{
public:
explicit GetCompletionRequest(const GetCompletionParams &params)
explicit GetCompletionRequest(const GetCompletionParams &params = {})
: Request(methodName, params)
{}
using Request::Request;