forked from qt-creator/qt-creator
Copilot: robustify request logic
Change-Id: Ifa46bc05f0bab8e3c7fc40d855a35e940f0628da Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
@@ -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
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <extensionsystem/iplugin.h>
|
||||
|
||||
namespace TextEditor { class TextEditorWidget; }
|
||||
|
||||
namespace Copilot {
|
||||
namespace Internal {
|
||||
|
||||
|
@@ -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
|
@@ -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
|
@@ -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 ¶ms)
|
||||
explicit GetCompletionRequest(const GetCompletionParams ¶ms = {})
|
||||
: Request(methodName, params)
|
||||
{}
|
||||
using Request::Request;
|
||||
|
Reference in New Issue
Block a user