From 625f0ef7262192ef9618740c2eeeebc4beedc463 Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Fri, 20 Jan 2023 07:09:01 +0100 Subject: [PATCH] Copilot: Add LSP plugin for Copilot Fixes: QTCREATORBUG-27771 Change-Id: I1249b9a4492427208a70b3e21bf20ac668fc3c50 Reviewed-by: hjk --- src/plugins/CMakeLists.txt | 1 + src/plugins/copilot/CMakeLists.txt | 17 ++ src/plugins/copilot/Copilot.json.in | 19 ++ src/plugins/copilot/authwidget.cpp | 163 ++++++++++++++++++ src/plugins/copilot/authwidget.h | 44 +++++ src/plugins/copilot/copilot.qbs | 30 ++++ src/plugins/copilot/copilotclient.cpp | 133 ++++++++++++++ src/plugins/copilot/copilotclient.h | 53 ++++++ src/plugins/copilot/copilotoptionspage.cpp | 46 +++++ src/plugins/copilot/copilotoptionspage.h | 25 +++ .../copilot/copilotoptionspagewidget.cpp | 36 ++++ .../copilot/copilotoptionspagewidget.h | 16 ++ src/plugins/copilot/copilotplugin.cpp | 37 ++++ src/plugins/copilot/copilotplugin.h | 30 ++++ src/plugins/copilot/copilotsettings.cpp | 65 +++++++ src/plugins/copilot/copilotsettings.h | 21 +++ src/plugins/copilot/copilottr.h | 15 ++ src/plugins/copilot/documentwatcher.cpp | 92 ++++++++++ src/plugins/copilot/documentwatcher.h | 31 ++++ src/plugins/copilot/requests/checkstatus.h | 54 ++++++ src/plugins/copilot/requests/getcompletions.h | 120 +++++++++++++ src/plugins/copilot/requests/signinconfirm.h | 36 ++++ src/plugins/copilot/requests/signininitiate.h | 43 +++++ src/plugins/copilot/requests/signout.h | 26 +++ src/plugins/plugins.qbs | 1 + 25 files changed, 1154 insertions(+) create mode 100644 src/plugins/copilot/CMakeLists.txt create mode 100644 src/plugins/copilot/Copilot.json.in create mode 100644 src/plugins/copilot/authwidget.cpp create mode 100644 src/plugins/copilot/authwidget.h create mode 100644 src/plugins/copilot/copilot.qbs create mode 100644 src/plugins/copilot/copilotclient.cpp create mode 100644 src/plugins/copilot/copilotclient.h create mode 100644 src/plugins/copilot/copilotoptionspage.cpp create mode 100644 src/plugins/copilot/copilotoptionspage.h create mode 100644 src/plugins/copilot/copilotoptionspagewidget.cpp create mode 100644 src/plugins/copilot/copilotoptionspagewidget.h create mode 100644 src/plugins/copilot/copilotplugin.cpp create mode 100644 src/plugins/copilot/copilotplugin.h create mode 100644 src/plugins/copilot/copilotsettings.cpp create mode 100644 src/plugins/copilot/copilotsettings.h create mode 100644 src/plugins/copilot/copilottr.h create mode 100644 src/plugins/copilot/documentwatcher.cpp create mode 100644 src/plugins/copilot/documentwatcher.h create mode 100644 src/plugins/copilot/requests/checkstatus.h create mode 100644 src/plugins/copilot/requests/getcompletions.h create mode 100644 src/plugins/copilot/requests/signinconfirm.h create mode 100644 src/plugins/copilot/requests/signininitiate.h create mode 100644 src/plugins/copilot/requests/signout.h diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index 8c4f2e2bb68..dc92077f0c1 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -100,3 +100,4 @@ add_subdirectory(qnx) add_subdirectory(webassembly) add_subdirectory(mcusupport) add_subdirectory(saferenderer) +add_subdirectory(copilot) diff --git a/src/plugins/copilot/CMakeLists.txt b/src/plugins/copilot/CMakeLists.txt new file mode 100644 index 00000000000..81e0cd34934 --- /dev/null +++ b/src/plugins/copilot/CMakeLists.txt @@ -0,0 +1,17 @@ +add_qtc_plugin(Copilot + SKIP_TRANSLATION + PLUGIN_DEPENDS Core LanguageClient + SOURCES + authwidget.cpp authwidget.h + copilotplugin.cpp copilotplugin.h + copilotclient.cpp copilotclient.h + copilotsettings.cpp copilotsettings.h + copilotoptionspage.cpp copilotoptionspage.h + copilotoptionspagewidget.cpp copilotoptionspagewidget.h + documentwatcher.cpp documentwatcher.h + requests/getcompletions.h + requests/checkstatus.h + requests/signout.h + requests/signininitiate.h + requests/signinconfirm.h +) diff --git a/src/plugins/copilot/Copilot.json.in b/src/plugins/copilot/Copilot.json.in new file mode 100644 index 00000000000..55ff6f6bf42 --- /dev/null +++ b/src/plugins/copilot/Copilot.json.in @@ -0,0 +1,19 @@ +{ + \"Name\" : \"Copilot\", + \"Version\" : \"$$QTCREATOR_VERSION\", + \"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\", + \"Experimental\" : true, + \"Vendor\" : \"The Qt Company Ltd\", + \"Copyright\" : \"(C) $$QTCREATOR_COPYRIGHT_YEAR The Qt Company Ltd\", + \"License\" : [ \"Commercial Usage\", + \"\", + \"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and The Qt Company.\", + \"\", + \"GNU General Public License Usage\", + \"\", + \"Alternatively, this plugin may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this plugin. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html.\" + ], + \"Description\" : \"Copilot support\", + \"Url\" : \"http://www.qt.io\", + $$dependencyList +} diff --git a/src/plugins/copilot/authwidget.cpp b/src/plugins/copilot/authwidget.cpp new file mode 100644 index 00000000000..9298bc66138 --- /dev/null +++ b/src/plugins/copilot/authwidget.cpp @@ -0,0 +1,163 @@ +// 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 "authwidget.h" + +#include "copilotclient.h" +#include "copilottr.h" + +#include +#include + +#include + +#include +#include + +using namespace LanguageClient; + +namespace Copilot { + +bool isCopilotClient(Client *client) +{ + return dynamic_cast(client) != nullptr; +} + +Internal::CopilotClient *coPilotClient(Client *client) +{ + return static_cast(client); +} + +Internal::CopilotClient *findClient() +{ + return Internal::CopilotClient::instance(); +} + +AuthWidget::AuthWidget(QWidget *parent) + : QWidget(parent) +{ + using namespace Utils::Layouting; + + m_button = new QPushButton(); + m_progressIndicator = new Utils::ProgressIndicator(Utils::ProgressIndicatorSize::Small); + m_statusLabel = new QLabel(); + + // clang-format off + Column { + Row { + m_button, m_progressIndicator, st + }, + m_statusLabel + }.attachTo(this); + // clang-format on + + setState("Checking status ...", true); + + connect(LanguageClientManager::instance(), + &LanguageClientManager::clientAdded, + this, + &AuthWidget::onClientAdded); + + connect(m_button, &QPushButton::clicked, this, [this]() { + if (m_status == Status::SignedIn) + signOut(); + else if (m_status == Status::SignedOut) + signIn(); + }); + + auto client = findClient(); + + if (client) + checkStatus(client); +} + +void AuthWidget::setState(const QString &buttonText, bool working) +{ + m_button->setText(buttonText); + m_progressIndicator->setVisible(working); + m_statusLabel->setVisible(!m_statusLabel->text().isEmpty()); + m_button->setEnabled(!working); +} + +void AuthWidget::onClientAdded(LanguageClient::Client *client) +{ + if (isCopilotClient(client)) { + checkStatus(coPilotClient(client)); + } +} + +void AuthWidget::checkStatus(Internal::CopilotClient *client) +{ + client->requestCheckStatus(false, [this](const CheckStatusRequest::Response &response) { + if (response.error()) { + setState("failed: " + response.error()->message(), false); + return; + } + const CheckStatusResponse result = *response.result(); + + if (result.user().isEmpty()) { + setState("Sign in", false); + m_status = Status::SignedOut; + return; + } + + setState("Sign out " + result.user(), false); + m_status = Status::SignedIn; + }); +} + +void AuthWidget::signIn() +{ + qCritical() << "Not implemented"; + auto client = findClient(); + QTC_ASSERT(client, return); + + setState("Signing in ...", true); + + client->requestSignInInitiate([this, client](const SignInInitiateRequest::Response &response) { + QTC_ASSERT(!response.error(), return); + + Utils::setClipboardAndSelection(response.result()->userCode()); + + QDesktopServices::openUrl(QUrl(response.result()->verificationUri())); + + m_statusLabel->setText(Tr::tr("A browser window will open, enter the code %1 when " + "asked.\nThe code has been copied to your clipboard.") + .arg(response.result()->userCode())); + m_statusLabel->setVisible(true); + + client->requestSignInConfirm(response.result()->userCode(), + [this](const SignInConfirmRequest::Response &response) { + m_statusLabel->setText(""); + + if (response.error()) { + QMessageBox::critical(this, + Tr::tr("Login failed"), + Tr::tr( + "The login request failed: ") + + response.error()->message()); + setState("Sign in", false); + return; + } + + setState("Sign Out " + response.result()->user(), false); + }); + }); +} + +void AuthWidget::signOut() +{ + auto client = findClient(); + QTC_ASSERT(client, return); + + setState("Signing out ...", true); + + client->requestSignOut([this, client](const SignOutRequest::Response &response) { + QTC_ASSERT(!response.error(), return); + QTC_ASSERT(response.result()->status() == "NotSignedIn", return); + + checkStatus(client); + }); +} + +} // namespace Copilot diff --git a/src/plugins/copilot/authwidget.h b/src/plugins/copilot/authwidget.h new file mode 100644 index 00000000000..9d0f8fdbbcf --- /dev/null +++ b/src/plugins/copilot/authwidget.h @@ -0,0 +1,44 @@ +// 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 +#include +#include + +namespace LanguageClient { +class Client; +} + +namespace Copilot { + +class AuthWidget : public QWidget +{ + Q_OBJECT + + enum class Status { SignedIn, SignedOut, Unknown }; + +public: + explicit AuthWidget(QWidget *parent = nullptr); + +private: + void onClientAdded(LanguageClient::Client *client); + void setState(const QString &buttonText, bool working); + void checkStatus(Internal::CopilotClient *client); + + void signIn(); + void signOut(); + +private: + Status m_status = Status::Unknown; + QPushButton *m_button = nullptr; + QLabel *m_statusLabel = nullptr; + Utils::ProgressIndicator *m_progressIndicator = nullptr; +}; + +} // namespace Copilot diff --git a/src/plugins/copilot/copilot.qbs b/src/plugins/copilot/copilot.qbs new file mode 100644 index 00000000000..fecc4be4997 --- /dev/null +++ b/src/plugins/copilot/copilot.qbs @@ -0,0 +1,30 @@ +import qbs 1.0 + +QtcPlugin { + name: "Copilot" + + Depends { name: "Core" } + Depends { name: "Qt"; submodules: ["widgets", "xml", "network"] } + + files: [ + "authwidget.cpp", + "authwidget.h", + "copilotplugin.cpp", + "copilotplugin.h", + "copilotclient.cpp", + "copilotclient.h", + "copilotsettings.cpp", + "copilotsettings.h", + "copilotoptionspage.cpp", + "copilotoptionspage.h", + "copilotoptionspagewidget.cpp", + "copilotoptionspagewidget.h", + "documentwatcher.cpp", + "documentwatcher.h", + "requests/getcompletions.h", + "requests/checkstatus.h", + "requests/signout.h", + "requests/signininitiate.h", + "requests/signinconfirm.h", + ] +} diff --git a/src/plugins/copilot/copilotclient.cpp b/src/plugins/copilot/copilotclient.cpp new file mode 100644 index 00000000000..de4653fcbd5 --- /dev/null +++ b/src/plugins/copilot/copilotclient.cpp @@ -0,0 +1,133 @@ +// 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 "copilotclient.h" + +#include "copilotsettings.h" +#include "documentwatcher.h" + +#include +#include +#include + +#include + +#include + +using namespace Utils; + +namespace Copilot::Internal { + +static LanguageClient::BaseClientInterface *clientInterface() +{ + const FilePath nodePath = CopilotSettings::instance().nodeJsPath.filePath(); + const FilePath distPath = CopilotSettings::instance().distPath.filePath(); + + CommandLine cmd{nodePath, {distPath.toFSPathString()}}; + + const auto interface = new LanguageClient::StdIOClientInterface; + interface->setCommandLine(cmd); + return interface; +} + +static CopilotClient *currentInstance = nullptr; + +CopilotClient *CopilotClient::instance() +{ + return currentInstance; +} + +CopilotClient::CopilotClient() + : LanguageClient::Client(clientInterface()) +{ + setName("Copilot"); + LanguageClient::LanguageFilter langFilter; + + langFilter.filePattern = {"*"}; + + 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)); + }); + + 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()); + }); + + currentInstance = this; +} + +void CopilotClient::requestCompletion( + const Utils::FilePath &path, + int version, + LanguageServerProtocol::Position position, + std::function callback) +{ + GetCompletionRequest request{ + {LanguageServerProtocol::TextDocumentIdentifier(hostPathToServerUri(path)), + version, + position}}; + request.setResponseCallback(callback); + + sendMessage(request); +} + +void CopilotClient::requestCheckStatus( + bool localChecksOnly, std::function callback) +{ + CheckStatusRequest request{localChecksOnly}; + request.setResponseCallback(callback); + + sendMessage(request); +} + +void CopilotClient::requestSignOut( + std::function callback) +{ + SignOutRequest request; + request.setResponseCallback(callback); + + sendMessage(request); +} + +void CopilotClient::requestSignInInitiate( + std::function callback) +{ + SignInInitiateRequest request; + request.setResponseCallback(callback); + + sendMessage(request); +} + +void CopilotClient::requestSignInConfirm( + const QString &userCode, + std::function callback) +{ + SignInConfirmRequest request(userCode); + request.setResponseCallback(callback); + + sendMessage(request); +} + +} // namespace Copilot::Internal diff --git a/src/plugins/copilot/copilotclient.h b/src/plugins/copilot/copilotclient.h new file mode 100644 index 00000000000..7521fc73b7b --- /dev/null +++ b/src/plugins/copilot/copilotclient.h @@ -0,0 +1,53 @@ +// 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 "requests/checkstatus.h" +#include "requests/getcompletions.h" +#include "requests/signinconfirm.h" +#include "requests/signininitiate.h" +#include "requests/signout.h" + +#include + +#include + +#include +#include + +namespace Copilot::Internal { + +class DocumentWatcher; + +class CopilotClient : public LanguageClient::Client +{ +public: + explicit CopilotClient(); + + static CopilotClient *instance(); + + void requestCompletion( + const Utils::FilePath &path, + int version, + LanguageServerProtocol::Position position, + std::function callback); + + void requestCheckStatus( + bool localChecksOnly, + std::function callback); + + void requestSignOut(std::function callback); + + void requestSignInInitiate( + std::function callback); + + void requestSignInConfirm( + const QString &userCode, + std::function callback); + +private: + std::map> m_documentWatchers; +}; + +} // namespace Copilot::Internal diff --git a/src/plugins/copilot/copilotoptionspage.cpp b/src/plugins/copilot/copilotoptionspage.cpp new file mode 100644 index 00000000000..67866fe8794 --- /dev/null +++ b/src/plugins/copilot/copilotoptionspage.cpp @@ -0,0 +1,46 @@ +// 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 "copilotoptionspage.h" + +#include "copilotoptionspagewidget.h" +#include "copilotsettings.h" + +#include + +namespace Copilot { + +CopilotOptionsPage::CopilotOptionsPage() +{ + setId("Copilot.General"); + setDisplayName("Copilot"); + setCategory("ZY.Copilot"); + setDisplayCategory("Copilot"); + + setCategoryIconPath(":/languageclient/images/settingscategory_languageclient.png"); +} + +CopilotOptionsPage::~CopilotOptionsPage() {} + +void CopilotOptionsPage::init() {} + +QWidget *CopilotOptionsPage::widget() +{ + return new CopilotOptionsPageWidget(); +} + +void CopilotOptionsPage::apply() +{ + CopilotSettings::instance().apply(); + CopilotSettings::instance().writeSettings(Core::ICore::settings()); +} + +void CopilotOptionsPage::finish() {} + +CopilotOptionsPage &CopilotOptionsPage::instance() +{ + static CopilotOptionsPage settingsPage; + return settingsPage; +} + +} // namespace Copilot diff --git a/src/plugins/copilot/copilotoptionspage.h b/src/plugins/copilot/copilotoptionspage.h new file mode 100644 index 00000000000..1124f74dea6 --- /dev/null +++ b/src/plugins/copilot/copilotoptionspage.h @@ -0,0 +1,25 @@ +// 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 + +namespace Copilot { + +class CopilotOptionsPage : public Core::IOptionsPage +{ +public: + CopilotOptionsPage(); + ~CopilotOptionsPage() override; + + static CopilotOptionsPage &instance(); + + void init(); + + QWidget *widget() override; + void apply() override; + void finish() override; +}; + +} // namespace Copilot diff --git a/src/plugins/copilot/copilotoptionspagewidget.cpp b/src/plugins/copilot/copilotoptionspagewidget.cpp new file mode 100644 index 00000000000..549ea7f9ec6 --- /dev/null +++ b/src/plugins/copilot/copilotoptionspagewidget.cpp @@ -0,0 +1,36 @@ +// 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 "copilotoptionspagewidget.h" + +#include "authwidget.h" +#include "copilotsettings.h" + +#include +#include + +using namespace Utils; +using namespace LanguageClient; + +namespace Copilot { + +CopilotOptionsPageWidget::CopilotOptionsPageWidget(QWidget *parent) + : QWidget(parent) +{ + using namespace Layouting; + + auto authWdgt = new AuthWidget(); + + // clang-format off + Column { + authWdgt, br, + CopilotSettings::instance().nodeJsPath, br, + CopilotSettings::instance().distPath, br, + st + }.attachTo(this); + // clang-format on +} + +CopilotOptionsPageWidget::~CopilotOptionsPageWidget() = default; + +} // namespace Copilot diff --git a/src/plugins/copilot/copilotoptionspagewidget.h b/src/plugins/copilot/copilotoptionspagewidget.h new file mode 100644 index 00000000000..37c51e68d3c --- /dev/null +++ b/src/plugins/copilot/copilotoptionspagewidget.h @@ -0,0 +1,16 @@ +// 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 + +namespace Copilot { +class CopilotOptionsPageWidget : public QWidget +{ + Q_OBJECT +public: + CopilotOptionsPageWidget(QWidget *parent = nullptr); + ~CopilotOptionsPageWidget() override; +}; +} // namespace Copilot diff --git a/src/plugins/copilot/copilotplugin.cpp b/src/plugins/copilot/copilotplugin.cpp new file mode 100644 index 00000000000..ff2c449088f --- /dev/null +++ b/src/plugins/copilot/copilotplugin.cpp @@ -0,0 +1,37 @@ +// 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 "copilotplugin.h" + +#include "copilotclient.h" +#include "copilotoptionspage.h" +#include "copilotsettings.h" + +#include + +using namespace Utils; + +namespace Copilot { +namespace Internal { + +void CopilotPlugin::initialize() +{ + CopilotSettings::instance().readSettings(Core::ICore::settings()); + + m_client = new CopilotClient(); + + connect(&CopilotSettings::instance(), &CopilotSettings::applied, this, [this]() { + if (m_client) + m_client->shutdown(); + m_client = nullptr; + m_client = new CopilotClient(); + }); +} + +void CopilotPlugin::extensionsInitialized() +{ + CopilotOptionsPage::instance().init(); +} + +} // namespace Internal +} // namespace Copilot diff --git a/src/plugins/copilot/copilotplugin.h b/src/plugins/copilot/copilotplugin.h new file mode 100644 index 00000000000..62f1d21f753 --- /dev/null +++ b/src/plugins/copilot/copilotplugin.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 "copilotclient.h" + +#include + +namespace Copilot { +namespace Internal { + +class CopilotPlugin : public ExtensionSystem::IPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Copilot.json") + +public: + CopilotPlugin() {} + ~CopilotPlugin() override {} + + void initialize() override; + void extensionsInitialized() override; + +private: + CopilotClient *m_client; +}; + +} // namespace Internal +} // namespace Copilot diff --git a/src/plugins/copilot/copilotsettings.cpp b/src/plugins/copilot/copilotsettings.cpp new file mode 100644 index 00000000000..ed298a0ab78 --- /dev/null +++ b/src/plugins/copilot/copilotsettings.cpp @@ -0,0 +1,65 @@ +// 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 "copilotsettings.h" +#include "copilottr.h" + +#include +#include + +using namespace Utils; + +namespace Copilot { + +CopilotSettings &CopilotSettings::instance() +{ + static CopilotSettings settings; + return settings; +} + +CopilotSettings::CopilotSettings() +{ + setAutoApply(false); + + const FilePath nodeFromPath = FilePath("node").searchInPath(); + + const FilePaths searchDirs + = {FilePath::fromUserInput("~/.vim/pack/github/start/copilot.vim/copilot/dist/agent.js"), + FilePath::fromUserInput( + "~/.config/nvim/pack/github/start/copilot.vim/copilot/dist/agent.js"), + FilePath::fromUserInput( + "~/vimfiles/pack/github/start/copilot.vim/copilot/dist/agent.js"), + FilePath::fromUserInput( + "~/AppData/Local/nvim/pack/github/start/copilot.vim/copilot/dist/agent.js")}; + + const FilePath distFromVim = Utils::findOrDefault(searchDirs, [](const FilePath &fp) { + return fp.exists(); + }); + + nodeJsPath.setExpectedKind(PathChooser::ExistingCommand); + nodeJsPath.setDefaultFilePath(nodeFromPath); + nodeJsPath.setSettingsKey("Copilot.NodeJsPath"); + nodeJsPath.setDisplayStyle(StringAspect::PathChooserDisplay); + nodeJsPath.setLabelText(Tr::tr("Node.js path:")); + nodeJsPath.setHistoryCompleter("Copilot.NodePath.History"); + nodeJsPath.setDisplayName(Tr::tr("Node.js Path")); + nodeJsPath.setToolTip( + Tr::tr("Select path to node.js executable. See https://nodejs.org/de/download/" + "for installation instructions.")); + + distPath.setExpectedKind(PathChooser::File); + distPath.setDefaultFilePath(distFromVim); + distPath.setSettingsKey("Copilot.DistPath"); + distPath.setDisplayStyle(StringAspect::PathChooserDisplay); + distPath.setLabelText(Tr::tr("Path to agent.js:")); + distPath.setToolTip(Tr::tr( + "Select path to agent.js in copilot neovim plugin. See " + "https://github.com/github/copilot.vim#getting-started for installation instructions.")); + distPath.setHistoryCompleter("Copilot.DistPath.History"); + distPath.setDisplayName(Tr::tr("Agent.js path")); + + registerAspect(&nodeJsPath); + registerAspect(&distPath); +} + +} // namespace Copilot diff --git a/src/plugins/copilot/copilotsettings.h b/src/plugins/copilot/copilotsettings.h new file mode 100644 index 00000000000..d089410216b --- /dev/null +++ b/src/plugins/copilot/copilotsettings.h @@ -0,0 +1,21 @@ +// 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 + +namespace Copilot { + +class CopilotSettings : public Utils::AspectContainer +{ +public: + CopilotSettings(); + + static CopilotSettings &instance(); + + Utils::StringAspect nodeJsPath; + Utils::StringAspect distPath; +}; + +} // namespace Copilot diff --git a/src/plugins/copilot/copilottr.h b/src/plugins/copilot/copilottr.h new file mode 100644 index 00000000000..a25a534f0d3 --- /dev/null +++ b/src/plugins/copilot/copilottr.h @@ -0,0 +1,15 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace Copilot { + +struct Tr +{ + Q_DECLARE_TR_FUNCTIONS(::Copilot) +}; + +} // namespace Copilot diff --git a/src/plugins/copilot/documentwatcher.cpp b/src/plugins/copilot/documentwatcher.cpp new file mode 100644 index 00000000000..f216826b44e --- /dev/null +++ b/src/plugins/copilot/documentwatcher.cpp @@ -0,0 +1,92 @@ +// 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() +{ + auto editor = Core::EditorManager::instance()->activateEditorForDocument(m_textDocument); + auto textEditorWidget = qobject_cast(editor->widget()); + if (!editor || !textEditorWidget) + return; + + auto cursor = textEditorWidget->multiTextCursor(); + if (cursor.hasMultipleCursors() || cursor.hasSelection()) + return; + + const int currentCursorPos = cursor.cursors().first().position(); + + m_client->requestCompletion( + m_textDocument->filePath(), + m_client->documentVersion(m_textDocument->filePath()), + Position(editor->currentLine() - 1, editor->currentColumn() - 1), + [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 auto list = result->completions().toList(); + + if (list.isEmpty()) + return; + + auto cursor = textEditorWidget->multiTextCursor(); + if (cursor.hasMultipleCursors() || cursor.hasSelection()) + return; + if (cursor.cursors().first().position() != currentCursorPos) + return; + + const auto firstCompletion = list.first(); + const QString content = firstCompletion.text().mid( + firstCompletion.position().character()); + + m_isEditing = true; + textEditorWidget->insertSuggestion(content); + m_isEditing = false; + /* + m_isEditing = true; + const auto &block = m_textDocument->document()->findBlockByLineNumber( + firstCompletion.position().line()); + m_textDocument->insertSuggestion(content, block); + m_isEditing = false; + */ + }); +} + +} // namespace Copilot::Internal diff --git a/src/plugins/copilot/documentwatcher.h b/src/plugins/copilot/documentwatcher.h new file mode 100644 index 00000000000..868be7956d4 --- /dev/null +++ b/src/plugins/copilot/documentwatcher.h @@ -0,0 +1,31 @@ +// 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/checkstatus.h b/src/plugins/copilot/requests/checkstatus.h new file mode 100644 index 00000000000..20e77eb1457 --- /dev/null +++ b/src/plugins/copilot/requests/checkstatus.h @@ -0,0 +1,54 @@ +// 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 +#include + +namespace Copilot { + +class CheckStatusParams : public LanguageServerProtocol::JsonObject +{ + static constexpr char16_t optionsKey[] = u"options"; + static constexpr char16_t localChecksOnlyKey[] = u"options"; + +public: + using JsonObject::JsonObject; + + CheckStatusParams(bool localChecksOnly = false) { setLocalChecksOnly(localChecksOnly); } + + void setLocalChecksOnly(bool localChecksOnly) + { + QJsonObject options; + options.insert(localChecksOnlyKey, localChecksOnly); + setOptions(options); + } + + void setOptions(QJsonObject options) { insert(optionsKey, options); } +}; + +class CheckStatusResponse : public LanguageServerProtocol::JsonObject +{ + static constexpr char16_t userKey[] = u"user"; + static constexpr char16_t statusKey[] = u"status"; + +public: + using JsonObject::JsonObject; + + QString status() const { return typedValue(statusKey); } + QString user() const { return typedValue(userKey); } +}; + +class CheckStatusRequest + : public LanguageServerProtocol::Request +{ +public: + explicit CheckStatusRequest(const CheckStatusParams ¶ms) + : Request(methodName, params) + {} + using Request::Request; + constexpr static const char methodName[] = "checkStatus"; +}; + +} // namespace Copilot diff --git a/src/plugins/copilot/requests/getcompletions.h b/src/plugins/copilot/requests/getcompletions.h new file mode 100644 index 00000000000..6a503071d8a --- /dev/null +++ b/src/plugins/copilot/requests/getcompletions.h @@ -0,0 +1,120 @@ +// 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 +#include +#include + +namespace Copilot { + +class Completion : public LanguageServerProtocol::JsonObject +{ + static constexpr char16_t displayTextKey[] = u"displayText"; + static constexpr char16_t uuidKey[] = u"uuid"; + +public: + using JsonObject::JsonObject; + + QString displayText() const { return typedValue(displayTextKey); } + LanguageServerProtocol::Position position() const + { + return typedValue(LanguageServerProtocol::positionKey); + } + LanguageServerProtocol::Range range() const + { + return typedValue(LanguageServerProtocol::rangeKey); + } + QString text() const { return typedValue(LanguageServerProtocol::textKey); } + QString uuid() const { return typedValue(uuidKey); } + + bool isValid() const override + { + return contains(LanguageServerProtocol::textKey) + && contains(LanguageServerProtocol::rangeKey) + && contains(LanguageServerProtocol::positionKey); + } +}; + +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) + { + setTextDocument(document); + setVersion(version); + setPosition(position); + } + using JsonObject::JsonObject; + + // The text document. + LanguageServerProtocol::TextDocumentIdentifier textDocument() const + { + return typedValue(docKey); + } + void setTextDocument(const LanguageServerProtocol::TextDocumentIdentifier &id) + { + insert(docKey, id); + } + + // The position inside the text document. + LanguageServerProtocol::Position position() const + { + return LanguageServerProtocol::fromJsonValue( + value(docKey).toObject().value(LanguageServerProtocol::positionKey)); + } + void setPosition(const LanguageServerProtocol::Position &position) + { + QJsonObject result = value(docKey).toObject(); + result[LanguageServerProtocol::positionKey] = (QJsonObject) position; + insert(docKey, result); + } + + // The version + int version() const { return typedValue(LanguageServerProtocol::versionKey); } + void setVersion(int version) + { + QJsonObject result = value(docKey).toObject(); + result[LanguageServerProtocol::versionKey] = version; + insert(docKey, result); + } + + bool isValid() const override + { + return contains(docKey) + && value(docKey).toObject().contains(LanguageServerProtocol::positionKey) + && value(docKey).toObject().contains(LanguageServerProtocol::versionKey); + } +}; + +class GetCompletionResponse : public LanguageServerProtocol::JsonObject +{ + static constexpr char16_t completionKey[] = u"completions"; + +public: + using JsonObject::JsonObject; + + LanguageServerProtocol::LanguageClientArray completions() const + { + return clientArray(completionKey); + } +}; + +class GetCompletionRequest + : public LanguageServerProtocol::Request +{ +public: + explicit GetCompletionRequest(const GetCompletionParams ¶ms) + : Request(methodName, params) + {} + using Request::Request; + constexpr static const char methodName[] = "getCompletionsCycling"; +}; + +} // namespace Copilot diff --git a/src/plugins/copilot/requests/signinconfirm.h b/src/plugins/copilot/requests/signinconfirm.h new file mode 100644 index 00000000000..64f4ce7d53d --- /dev/null +++ b/src/plugins/copilot/requests/signinconfirm.h @@ -0,0 +1,36 @@ +// 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 "checkstatus.h" + +#include +#include + +namespace Copilot { + +class SignInConfirmParams : public LanguageServerProtocol::JsonObject +{ + static constexpr char16_t userCodeKey[] = u"userCode"; + +public: + using JsonObject::JsonObject; + + SignInConfirmParams(const QString &userCode) { setUserCode(userCode); } + + void setUserCode(const QString &userCode) { insert(userCodeKey, userCode); } +}; + +class SignInConfirmRequest + : public LanguageServerProtocol::Request +{ +public: + explicit SignInConfirmRequest(const QString &userCode) + : Request(methodName, {userCode}) + {} + using Request::Request; + constexpr static const char methodName[] = "signInConfirm"; +}; + +} // namespace Copilot diff --git a/src/plugins/copilot/requests/signininitiate.h b/src/plugins/copilot/requests/signininitiate.h new file mode 100644 index 00000000000..005205e6e01 --- /dev/null +++ b/src/plugins/copilot/requests/signininitiate.h @@ -0,0 +1,43 @@ +// 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 + +// 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 +#include + +namespace Copilot { + +using SignInInitiateParams = LanguageServerProtocol::JsonObject; + +class SignInInitiateResponse : public LanguageServerProtocol::JsonObject +{ + static constexpr char16_t verificationUriKey[] = u"verificationUri"; + static constexpr char16_t userCodeKey[] = u"userCode"; + +public: + using JsonObject::JsonObject; + +public: + QString verificationUri() const { return typedValue(verificationUriKey); } + QString userCode() const { return typedValue(userCodeKey); } +}; + +class SignInInitiateRequest : public LanguageServerProtocol::Request +{ +public: + explicit SignInInitiateRequest() + : Request(methodName, {}) + {} + using Request::Request; + constexpr static const char methodName[] = "signInInitiate"; +}; + +} // namespace Copilot diff --git a/src/plugins/copilot/requests/signout.h b/src/plugins/copilot/requests/signout.h new file mode 100644 index 00000000000..944c10d414b --- /dev/null +++ b/src/plugins/copilot/requests/signout.h @@ -0,0 +1,26 @@ +// 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 "checkstatus.h" + +#include +#include + +namespace Copilot { + +using SignOutParams = LanguageServerProtocol::JsonObject; + +class SignOutRequest + : public LanguageServerProtocol::Request +{ +public: + explicit SignOutRequest() + : Request(methodName, {}) + {} + using Request::Request; + constexpr static const char methodName[] = "signOut"; +}; + +} // namespace Copilot diff --git a/src/plugins/plugins.qbs b/src/plugins/plugins.qbs index b878e76c3e3..c38d0d8123c 100644 --- a/src/plugins/plugins.qbs +++ b/src/plugins/plugins.qbs @@ -85,5 +85,6 @@ Project { "vcsbase/vcsbase.qbs", "webassembly/webassembly.qbs", "welcome/welcome.qbs" + "copilot/copilot.qbs" ].concat(project.additionalPlugins) }