diff --git a/src/plugins/copilot/CMakeLists.txt b/src/plugins/copilot/CMakeLists.txt index f0e29df2cf9..8e8526542a7 100644 --- a/src/plugins/copilot/CMakeLists.txt +++ b/src/plugins/copilot/CMakeLists.txt @@ -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 diff --git a/src/plugins/copilot/copilotclient.cpp b/src/plugins/copilot/copilotclient.cpp index de4653fcbd5..95fa78d2630 100644 --- a/src/plugins/copilot/copilotclient.cpp +++ b/src/plugins/copilot/copilotclient.cpp @@ -4,7 +4,6 @@ #include "copilotclient.h" #include "copilotsettings.h" -#include "documentwatcher.h" #include #include @@ -14,6 +13,14 @@ #include +#include + +#include + +#include + +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( - document); - if (!textDocument) - return; - - openDocument(textDocument); - - m_documentWatchers.emplace(textDocument->filePath(), - std::make_unique(this, textDocument)); - }); + auto openDoc = [this](Core::IDocument *document) { + if (auto *textDocument = qobject_cast(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(document); - if (!textDocument) - return; - - closeDocument(textDocument); - m_documentWatchers.erase(textDocument->filePath()); + if (auto textDocument = qobject_cast(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 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(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 result = response.result()) { + LanguageClientArray 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 callback) { diff --git a/src/plugins/copilot/copilotclient.h b/src/plugins/copilot/copilotclient.h index 7521fc73b7b..73fe95e2531 100644 --- a/src/plugins/copilot/copilotclient.h +++ b/src/plugins/copilot/copilotclient.h @@ -27,11 +27,13 @@ public: static CopilotClient *instance(); - void requestCompletion( - const Utils::FilePath &path, - int version, - LanguageServerProtocol::Position position, - std::function 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 callback); private: - std::map> m_documentWatchers; + QMap m_runningRequests; + struct ScheduleData + { + int cursorPosition = -1; + QTimer *timer = nullptr; + }; + QMap m_scheduledRequests; }; } // namespace Copilot::Internal diff --git a/src/plugins/copilot/copilotplugin.cpp b/src/plugins/copilot/copilotplugin.cpp index 910bfa33b7b..d473cac2cbc 100644 --- a/src/plugins/copilot/copilotplugin.cpp +++ b/src/plugins/copilot/copilotplugin.cpp @@ -8,8 +8,12 @@ #include "copilotsettings.h" #include +#include + +#include 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(); diff --git a/src/plugins/copilot/copilotplugin.h b/src/plugins/copilot/copilotplugin.h index d81d2e86282..798db7c2819 100644 --- a/src/plugins/copilot/copilotplugin.h +++ b/src/plugins/copilot/copilotplugin.h @@ -7,6 +7,8 @@ #include +namespace TextEditor { class TextEditorWidget; } + namespace Copilot { namespace Internal { diff --git a/src/plugins/copilot/documentwatcher.cpp b/src/plugins/copilot/documentwatcher.cpp deleted file mode 100644 index 69b201b5216..00000000000 --- a/src/plugins/copilot/documentwatcher.cpp +++ /dev/null @@ -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 - -#include - -#include - -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(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 result = response.result(); - QTC_ASSERT(result, return); - - const QList 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 diff --git a/src/plugins/copilot/documentwatcher.h b/src/plugins/copilot/documentwatcher.h deleted file mode 100644 index 868be7956d4..00000000000 --- a/src/plugins/copilot/documentwatcher.h +++ /dev/null @@ -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 - -#include - -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 diff --git a/src/plugins/copilot/requests/getcompletions.h b/src/plugins/copilot/requests/getcompletions.h index 6a503071d8a..d602fea97d3 100644 --- a/src/plugins/copilot/requests/getcompletions.h +++ b/src/plugins/copilot/requests/getcompletions.h @@ -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 { public: - explicit GetCompletionRequest(const GetCompletionParams ¶ms) + explicit GetCompletionRequest(const GetCompletionParams ¶ms = {}) : Request(methodName, params) {} using Request::Request;