From 49ac087955789e4ab931afe3a34414c7cba68589 Mon Sep 17 00:00:00 2001 From: David Schulz Date: Mon, 13 Dec 2021 14:19:30 +0100 Subject: [PATCH] Python: move language client functionality out of utils There will be more lsp specific functionality so moving it into its own space is reasonable. Change-Id: Ic87d437182d68673b53f662c804707138fef5b6c Reviewed-by: Christian Stenger --- src/plugins/python/CMakeLists.txt | 7 +- src/plugins/python/python.pro | 14 +- src/plugins/python/python.qbs | 14 +- src/plugins/python/pythonlanguageclient.cpp | 451 ++++++++++++++++++ src/plugins/python/pythonlanguageclient.h | 69 +++ src/plugins/python/pythonplugin.cpp | 2 +- src/plugins/python/pythonrunconfiguration.cpp | 2 +- src/plugins/python/pythonutils.cpp | 425 +---------------- src/plugins/python/pythonutils.h | 40 +- 9 files changed, 547 insertions(+), 477 deletions(-) create mode 100644 src/plugins/python/pythonlanguageclient.cpp create mode 100644 src/plugins/python/pythonlanguageclient.h diff --git a/src/plugins/python/CMakeLists.txt b/src/plugins/python/CMakeLists.txt index 309680913a9..42984eb91ee 100644 --- a/src/plugins/python/CMakeLists.txt +++ b/src/plugins/python/CMakeLists.txt @@ -3,15 +3,16 @@ add_qtc_plugin(Python PLUGIN_DEPENDS Core LanguageClient ProjectExplorer TextEditor SOURCES python.qrc - pythoneditor.cpp pythoneditor.h pythonconstants.h - pythonplugin.cpp pythonplugin.h + pythoneditor.cpp pythoneditor.h pythonformattoken.h pythonhighlighter.cpp pythonhighlighter.h pythonindenter.cpp pythonindenter.h + pythonlanguageclient.cpp pythonlanguageclient.h + pythonplugin.cpp pythonplugin.h pythonproject.cpp pythonproject.h pythonrunconfiguration.cpp pythonrunconfiguration.h - pythonsettings.cpp pythonsettings.h pythonscanner.cpp pythonscanner.h + pythonsettings.cpp pythonsettings.h pythonutils.cpp pythonutils.h ) diff --git a/src/plugins/python/python.pro b/src/plugins/python/python.pro index 29c53be779e..6c2e872bf1c 100644 --- a/src/plugins/python/python.pro +++ b/src/plugins/python/python.pro @@ -4,28 +4,30 @@ DEFINES += \ PYTHON_LIBRARY HEADERS += \ - pythonplugin.h \ - pythoneditor.h \ pythonconstants.h \ + pythoneditor.h \ + pythonformattoken.h \ pythonhighlighter.h \ pythonindenter.h \ - pythonformattoken.h \ + pythonlanguageclient.h \ + pythonplugin.h \ pythonproject.h \ pythonrunconfiguration.h \ pythonscanner.h \ pythonsettings.h \ - pythonutils.h + pythonutils.h \ SOURCES += \ - pythonplugin.cpp \ pythoneditor.cpp \ pythonhighlighter.cpp \ pythonindenter.cpp \ + pythonlanguageclient.cpp \ + pythonplugin.cpp \ pythonproject.cpp \ pythonrunconfiguration.cpp \ pythonscanner.cpp \ pythonsettings.cpp \ - pythonutils.cpp + pythonutils.cpp \ RESOURCES += \ python.qrc diff --git a/src/plugins/python/python.qbs b/src/plugins/python/python.qbs index ba5e6b39e11..14ec87b08fe 100644 --- a/src/plugins/python/python.qbs +++ b/src/plugins/python/python.qbs @@ -18,22 +18,24 @@ QtcPlugin { name: "General" files: [ "python.qrc", + "pythonconstants.h", "pythoneditor.cpp", "pythoneditor.h", - "pythonconstants.h", - "pythonplugin.cpp", - "pythonplugin.h", - "pythonhighlighter.h", + "pythonformattoken.h", "pythonhighlighter.cpp", + "pythonhighlighter.h", "pythonindenter.cpp", "pythonindenter.h", - "pythonformattoken.h", + "pythonlanguageclient.cpp", + "pythonlanguageclient.h", + "pythonplugin.cpp", + "pythonplugin.h", "pythonproject.cpp", "pythonproject.h", "pythonrunconfiguration.cpp", "pythonrunconfiguration.h", - "pythonscanner.h", "pythonscanner.cpp", + "pythonscanner.h", "pythonsettings.cpp", "pythonsettings.h", "pythonutils.cpp", diff --git a/src/plugins/python/pythonlanguageclient.cpp b/src/plugins/python/pythonlanguageclient.cpp new file mode 100644 index 00000000000..a0292f67978 --- /dev/null +++ b/src/plugins/python/pythonlanguageclient.cpp @@ -0,0 +1,451 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the 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. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file 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 file. 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. +** +****************************************************************************/ + +#include "pythonlanguageclient.h" + +#include "pythonconstants.h" +#include "pythonplugin.h" +#include "pythonutils.h" + +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include + +using namespace LanguageClient; +using namespace Utils; + +namespace Python { +namespace Internal { + +static constexpr char startPylsInfoBarId[] = "Python::StartPyls"; +static constexpr char installPylsInfoBarId[] = "Python::InstallPyls"; +static constexpr char enablePylsInfoBarId[] = "Python::EnablePyls"; +static constexpr char installPylsTaskId[] = "Python::InstallPylsTask"; + +struct PythonLanguageServerState +{ + enum { + CanNotBeInstalled, + CanBeInstalled, + AlreadyInstalled, + AlreadyConfigured, + ConfiguredButDisabled + } state; + FilePath pylsModulePath; +}; + +static QString pythonName(const FilePath &pythonPath) +{ + static QHash nameForPython; + if (!pythonPath.exists()) + return {}; + QString name = nameForPython.value(pythonPath); + if (name.isEmpty()) { + QtcProcess pythonProcess; + pythonProcess.setTimeoutS(2); + pythonProcess.setCommand({pythonPath, {"--version"}}); + pythonProcess.runBlocking(); + if (pythonProcess.result() != QtcProcess::FinishedWithSuccess) + return {}; + name = pythonProcess.allOutput().trimmed(); + nameForPython[pythonPath] = name; + } + return name; +} + +FilePath getPylsModulePath(CommandLine pylsCommand) +{ + static QMutex mutex; // protect the access to the cache + QMutexLocker locker(&mutex); + static QMap cache; + const FilePath &modulePath = cache.value(pylsCommand.executable()); + if (!modulePath.isEmpty()) + return modulePath; + + pylsCommand.addArg("-h"); + + QtcProcess pythonProcess; + Environment env = pythonProcess.environment(); + env.set("PYTHONVERBOSE", "x"); + pythonProcess.setEnvironment(env); + pythonProcess.setCommand(pylsCommand); + pythonProcess.runBlocking(); + + static const QString pylsInitPattern = "(.*)" + + QRegularExpression::escape( + QDir::toNativeSeparators("/pylsp/__init__.py")) + + '$'; + static const QRegularExpression regexCached(" matches " + pylsInitPattern, + QRegularExpression::MultilineOption); + static const QRegularExpression regexNotCached(" code object from " + pylsInitPattern, + QRegularExpression::MultilineOption); + + const QString output = pythonProcess.allOutput(); + for (const auto ®ex : {regexCached, regexNotCached}) { + const QRegularExpressionMatch result = regex.match(output); + if (result.hasMatch()) { + const FilePath &modulePath = FilePath::fromUserInput(result.captured(1)); + cache[pylsCommand.executable()] = modulePath; + return modulePath; + } + } + return {}; +} + +QList configuredPythonLanguageServer() +{ + using namespace LanguageClient; + QList result; + for (const BaseSettings *setting : LanguageClientManager::currentSettings()) { + if (setting->m_languageFilter.isSupported("foo.py", Constants::C_PY_MIMETYPE)) + result << dynamic_cast(setting); + } + return result; +} + +static PythonLanguageServerState checkPythonLanguageServer(const FilePath &python) +{ + using namespace LanguageClient; + const CommandLine pythonLShelpCommand(python, {"-m", "pylsp", "-h"}); + const FilePath &modulePath = getPylsModulePath(pythonLShelpCommand); + for (const StdIOSettings *serverSetting : configuredPythonLanguageServer()) { + if (modulePath == getPylsModulePath(serverSetting->command())) { + return {serverSetting->m_enabled ? PythonLanguageServerState::AlreadyConfigured + : PythonLanguageServerState::ConfiguredButDisabled, + FilePath()}; + } + } + + QtcProcess pythonProcess; + pythonProcess.setCommand(pythonLShelpCommand); + pythonProcess.runBlocking(); + if (pythonProcess.allOutput().contains("Python Language Server")) + return {PythonLanguageServerState::AlreadyInstalled, modulePath}; + + pythonProcess.setCommand({python, {"-m", "pip", "-V"}}); + pythonProcess.runBlocking(); + if (pythonProcess.allOutput().startsWith("pip ")) + return {PythonLanguageServerState::CanBeInstalled, FilePath()}; + else + return {PythonLanguageServerState::CanNotBeInstalled, FilePath()}; +} + +PyLSConfigureAssistant *PyLSConfigureAssistant::instance() +{ + static auto *instance = new PyLSConfigureAssistant(PythonPlugin::instance()); + return instance; +} + +const StdIOSettings *PyLSConfigureAssistant::languageServerForPython(const FilePath &python) +{ + return findOrDefault(configuredPythonLanguageServer(), + [pythonModulePath = getPylsModulePath( + CommandLine(python, {"-m", "pylsp"}))](const StdIOSettings *setting) { + return getPylsModulePath(setting->command()) == pythonModulePath; + }); +} + +static Client *registerLanguageServer(const FilePath &python) +{ + auto *settings = new StdIOSettings(); + settings->m_executable = python; + settings->m_arguments = "-m pylsp"; + settings->m_name = PyLSConfigureAssistant::tr("Python Language Server (%1)") + .arg(pythonName(python)); + settings->m_languageFilter.mimeTypes = QStringList(Constants::C_PY_MIMETYPE); + LanguageClientManager::registerClientSettings(settings); + Client *client = LanguageClientManager::clientForSetting(settings).value(0); + PyLSConfigureAssistant::updateEditorInfoBars(python, client); + return client; +} + +class PythonLSInstallHelper : public QObject +{ + Q_OBJECT +public: + PythonLSInstallHelper(const FilePath &python, QPointer document) + : m_python(python) + , m_document(document) + { + m_watcher.setFuture(m_future.future()); + } + + void run() + { + Core::ProgressManager::addTask(m_future.future(), "Install PyLS", installPylsTaskId); + connect(&m_process, &QtcProcess::finished, this, &PythonLSInstallHelper::installFinished); + connect(&m_process, + &QtcProcess::readyReadStandardError, + this, + &PythonLSInstallHelper::errorAvailable); + connect(&m_process, + &QtcProcess::readyReadStandardOutput, + this, + &PythonLSInstallHelper::outputAvailable); + + connect(&m_killTimer, &QTimer::timeout, this, &PythonLSInstallHelper::cancel); + connect(&m_watcher, &QFutureWatcher::canceled, this, &PythonLSInstallHelper::cancel); + + QStringList arguments = {"-m", "pip", "install", "python-lsp-server[all]"}; + + // add --user to global pythons, but skip it for venv pythons + if (!QDir(m_python.parentDir().toString()).exists("activate")) + arguments << "--user"; + + m_process.setCommand({m_python, arguments}); + m_process.start(); + + Core::MessageManager::writeDisrupting( + tr("Running \"%1\" to install Python language server.") + .arg(m_process.commandLine().toUserOutput())); + + m_killTimer.setSingleShot(true); + m_killTimer.start(5 /*minutes*/ * 60 * 1000); + } + +private: + void cancel() + { + m_process.stopProcess(); + Core::MessageManager::writeFlashing( + tr("The Python language server installation was canceled by %1.") + .arg(m_killTimer.isActive() ? tr("user") : tr("time out"))); + } + + void installFinished() + { + m_future.reportFinished(); + if (m_process.result() == QtcProcess::FinishedWithSuccess) { + if (Client *client = registerLanguageServer(m_python)) + LanguageClientManager::openDocumentWithClient(m_document, client); + } else { + Core::MessageManager::writeFlashing( + tr("Installing the Python language server failed with exit code %1") + .arg(m_process.exitCode())); + } + deleteLater(); + } + + void outputAvailable() + { + const QString &stdOut = QString::fromLocal8Bit(m_process.readAllStandardOutput().trimmed()); + if (!stdOut.isEmpty()) + Core::MessageManager::writeSilently(stdOut); + } + + void errorAvailable() + { + const QString &stdErr = QString::fromLocal8Bit(m_process.readAllStandardError().trimmed()); + if (!stdErr.isEmpty()) + Core::MessageManager::writeSilently(stdErr); + } + + QFutureInterface m_future; + QFutureWatcher m_watcher; + QtcProcess m_process; + QTimer m_killTimer; + const FilePath m_python; + QPointer m_document; +}; + +void PyLSConfigureAssistant::installPythonLanguageServer(const FilePath &python, + QPointer document) +{ + document->infoBar()->removeInfo(installPylsInfoBarId); + + // Hide all install info bar entries for this python, but keep them in the list + // so the language server will be setup properly after the installation is done. + for (TextEditor::TextDocument *additionalDocument : m_infoBarEntries[python]) + additionalDocument->infoBar()->removeInfo(installPylsInfoBarId); + + auto install = new PythonLSInstallHelper(python, document); + install->run(); +} + +static void setupPythonLanguageServer(const FilePath &python, + QPointer document) +{ + document->infoBar()->removeInfo(startPylsInfoBarId); + if (Client *client = registerLanguageServer(python)) + LanguageClientManager::openDocumentWithClient(document, client); +} + +static void enablePythonLanguageServer(const FilePath &python, + QPointer document) +{ + document->infoBar()->removeInfo(enablePylsInfoBarId); + if (const StdIOSettings *setting = PyLSConfigureAssistant::languageServerForPython(python)) { + LanguageClientManager::enableClientSettings(setting->m_id); + if (const StdIOSettings *setting = PyLSConfigureAssistant::languageServerForPython(python)) { + if (Client *client = LanguageClientManager::clientForSetting(setting).value(0)) { + LanguageClientManager::openDocumentWithClient(document, client); + PyLSConfigureAssistant::updateEditorInfoBars(python, client); + } + } + } +} + +void PyLSConfigureAssistant::documentOpened(Core::IDocument *document) +{ + auto textDocument = qobject_cast(document); + if (!textDocument || textDocument->mimeType() != Constants::C_PY_MIMETYPE) + return; + + const FilePath &python = detectPython(textDocument->filePath()); + if (!python.exists()) + return; + + instance()->openDocumentWithPython(python, textDocument); +} + +void PyLSConfigureAssistant::openDocumentWithPython(const FilePath &python, + TextEditor::TextDocument *document) +{ + using CheckPylsWatcher = QFutureWatcher; + + QPointer watcher = new CheckPylsWatcher(); + + // cancel and delete watcher after a 10 second timeout + QTimer::singleShot(10000, this, [watcher]() { + if (watcher) { + watcher->cancel(); + watcher->deleteLater(); + } + }); + + connect(watcher, + &CheckPylsWatcher::resultReadyAt, + this, + [=, document = QPointer(document)]() { + if (!document || !watcher) + return; + handlePyLSState(python, watcher->result(), document); + watcher->deleteLater(); + }); + watcher->setFuture(Utils::runAsync(&checkPythonLanguageServer, python)); +} + +void PyLSConfigureAssistant::handlePyLSState(const FilePath &python, + const PythonLanguageServerState &state, + TextEditor::TextDocument *document) +{ + if (state.state == PythonLanguageServerState::CanNotBeInstalled) + return; + if (state.state == PythonLanguageServerState::AlreadyConfigured) { + if (const StdIOSettings *setting = languageServerForPython(python)) { + if (Client *client = LanguageClientManager::clientForSetting(setting).value(0)) + LanguageClientManager::openDocumentWithClient(document, client); + } + return; + } + + resetEditorInfoBar(document); + Utils::InfoBar *infoBar = document->infoBar(); + if (state.state == PythonLanguageServerState::CanBeInstalled + && infoBar->canInfoBeAdded(installPylsInfoBarId)) { + auto message = tr("Install and set up Python language server (PyLS) for %1 (%2). " + "The language server provides Python specific completion and annotation.") + .arg(pythonName(python), python.toUserOutput()); + Utils::InfoBarEntry info(installPylsInfoBarId, + message, + Utils::InfoBarEntry::GlobalSuppression::Enabled); + info.setCustomButtonInfo(tr("Install"), + [=]() { installPythonLanguageServer(python, document); }); + infoBar->addInfo(info); + m_infoBarEntries[python] << document; + } else if (state.state == PythonLanguageServerState::AlreadyInstalled + && infoBar->canInfoBeAdded(startPylsInfoBarId)) { + auto message = tr("Found a Python language server for %1 (%2). " + "Set it up for this document?") + .arg(pythonName(python), python.toUserOutput()); + Utils::InfoBarEntry info(startPylsInfoBarId, + message, + Utils::InfoBarEntry::GlobalSuppression::Enabled); + info.setCustomButtonInfo(tr("Set Up"), + [=]() { setupPythonLanguageServer(python, document); }); + infoBar->addInfo(info); + m_infoBarEntries[python] << document; + } else if (state.state == PythonLanguageServerState::ConfiguredButDisabled + && infoBar->canInfoBeAdded(enablePylsInfoBarId)) { + auto message = tr("Enable Python language server for %1 (%2)?") + .arg(pythonName(python), python.toUserOutput()); + Utils::InfoBarEntry info(enablePylsInfoBarId, + message, + Utils::InfoBarEntry::GlobalSuppression::Enabled); + info.setCustomButtonInfo(tr("Enable"), + [=]() { enablePythonLanguageServer(python, document); }); + infoBar->addInfo(info); + m_infoBarEntries[python] << document; + } +} + +void PyLSConfigureAssistant::updateEditorInfoBars(const FilePath &python, Client *client) +{ + for (TextEditor::TextDocument *document : instance()->m_infoBarEntries.take(python)) { + instance()->resetEditorInfoBar(document); + if (client) + LanguageClientManager::openDocumentWithClient(document, client); + } +} + +void PyLSConfigureAssistant::resetEditorInfoBar(TextEditor::TextDocument *document) +{ + for (QList &documents : m_infoBarEntries) + documents.removeAll(document); + Utils::InfoBar *infoBar = document->infoBar(); + infoBar->removeInfo(installPylsInfoBarId); + infoBar->removeInfo(startPylsInfoBarId); + infoBar->removeInfo(enablePylsInfoBarId); +} + +PyLSConfigureAssistant::PyLSConfigureAssistant(QObject *parent) + : QObject(parent) +{ + Core::EditorManager::instance(); + + connect(Core::EditorManager::instance(), + &Core::EditorManager::documentClosed, + this, + [this](Core::IDocument *document) { + if (auto textDocument = qobject_cast(document)) + resetEditorInfoBar(textDocument); + }); +} + +} // namespace Internal +} // namespace Python + +#include "pythonlanguageclient.moc" diff --git a/src/plugins/python/pythonlanguageclient.h b/src/plugins/python/pythonlanguageclient.h new file mode 100644 index 00000000000..30bc834a516 --- /dev/null +++ b/src/plugins/python/pythonlanguageclient.h @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the 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. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file 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 file. 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. +** +****************************************************************************/ + +#pragma once + +#include + +namespace Core { class IDocument; } +namespace LanguageClient { +class Client; +class StdIOSettings; +} +namespace TextEditor { class TextDocument; } + +namespace Python { +namespace Internal { + +struct PythonLanguageServerState; + +class PyLSConfigureAssistant : public QObject +{ + Q_OBJECT +public: + static PyLSConfigureAssistant *instance(); + + static const LanguageClient::StdIOSettings *languageServerForPython( + const Utils::FilePath &python); + static void documentOpened(Core::IDocument *document); + static void updateEditorInfoBars(const Utils::FilePath &python, LanguageClient::Client *client); + + void openDocumentWithPython(const Utils::FilePath &python, TextEditor::TextDocument *document); + +private: + explicit PyLSConfigureAssistant(QObject *parent); + + void handlePyLSState(const Utils::FilePath &python, + const PythonLanguageServerState &state, + TextEditor::TextDocument *document); + void resetEditorInfoBar(TextEditor::TextDocument *document); + void installPythonLanguageServer(const Utils::FilePath &python, + QPointer document); + + QHash> m_infoBarEntries; +}; + +} // namespace Internal +} // namespace Python diff --git a/src/plugins/python/pythonplugin.cpp b/src/plugins/python/pythonplugin.cpp index 9c02a7ead2b..dc89abf2b02 100644 --- a/src/plugins/python/pythonplugin.cpp +++ b/src/plugins/python/pythonplugin.cpp @@ -26,10 +26,10 @@ #include "pythonplugin.h" #include "pythoneditor.h" +#include "pythonlanguageclient.h" #include "pythonproject.h" #include "pythonsettings.h" #include "pythonrunconfiguration.h" -#include "pythonutils.h" #include diff --git a/src/plugins/python/pythonrunconfiguration.cpp b/src/plugins/python/pythonrunconfiguration.cpp index 19d73ce77ef..4c49a46eb2b 100644 --- a/src/plugins/python/pythonrunconfiguration.cpp +++ b/src/plugins/python/pythonrunconfiguration.cpp @@ -26,9 +26,9 @@ #include "pythonrunconfiguration.h" #include "pythonconstants.h" +#include "pythonlanguageclient.h" #include "pythonproject.h" #include "pythonsettings.h" -#include "pythonutils.h" #include #include diff --git a/src/plugins/python/pythonutils.cpp b/src/plugins/python/pythonutils.cpp index a98fdc449a3..74294263a93 100644 --- a/src/plugins/python/pythonutils.cpp +++ b/src/plugins/python/pythonutils.cpp @@ -25,155 +25,26 @@ #include "pythonutils.h" -#include "pythonconstants.h" -#include "pythonplugin.h" #include "pythonproject.h" #include "pythonrunconfiguration.h" #include "pythonsettings.h" -#include -#include - -#include -#include +#include +#include #include #include -#include - +#include #include -#include #include -#include -#include -#include -#include -#include -#include -#include - -using namespace LanguageClient; using namespace Utils; namespace Python { namespace Internal { -static constexpr char startPylsInfoBarId[] = "Python::StartPyls"; -static constexpr char installPylsInfoBarId[] = "Python::InstallPyls"; -static constexpr char enablePylsInfoBarId[] = "Python::EnablePyls"; -static constexpr char installPylsTaskId[] = "Python::InstallPylsTask"; - -struct PythonLanguageServerState -{ - enum { - CanNotBeInstalled, - CanBeInstalled, - AlreadyInstalled, - AlreadyConfigured, - ConfiguredButDisabled - } state; - FilePath pylsModulePath; -}; - -static QString pythonName(const FilePath &pythonPath) -{ - static QHash nameForPython; - if (!pythonPath.exists()) - return {}; - QString name = nameForPython.value(pythonPath); - if (name.isEmpty()) { - QtcProcess pythonProcess; - pythonProcess.setTimeoutS(2); - pythonProcess.setCommand({pythonPath, {"--version"}}); - pythonProcess.runBlocking(); - if (pythonProcess.result() != QtcProcess::FinishedWithSuccess) - return {}; - name = pythonProcess.allOutput().trimmed(); - nameForPython[pythonPath] = name; - } - return name; -} - -FilePath getPylsModulePath(CommandLine pylsCommand) -{ - static QMutex mutex; // protect the access to the cache - QMutexLocker locker(&mutex); - static QMap cache; - const FilePath &modulePath = cache.value(pylsCommand.executable()); - if (!modulePath.isEmpty()) - return modulePath; - - pylsCommand.addArg("-h"); - - QtcProcess pythonProcess; - Environment env = pythonProcess.environment(); - env.set("PYTHONVERBOSE", "x"); - pythonProcess.setEnvironment(env); - pythonProcess.setCommand(pylsCommand); - pythonProcess.runBlocking(); - - static const QString pylsInitPattern = "(.*)" - + QRegularExpression::escape( - QDir::toNativeSeparators("/pylsp/__init__.py")) - + '$'; - static const QRegularExpression regexCached(" matches " + pylsInitPattern, - QRegularExpression::MultilineOption); - static const QRegularExpression regexNotCached(" code object from " + pylsInitPattern, - QRegularExpression::MultilineOption); - - const QString output = pythonProcess.allOutput(); - for (const auto ®ex : {regexCached, regexNotCached}) { - const QRegularExpressionMatch result = regex.match(output); - if (result.hasMatch()) { - const FilePath &modulePath = FilePath::fromUserInput(result.captured(1)); - cache[pylsCommand.executable()] = modulePath; - return modulePath; - } - } - return {}; -} - -QList configuredPythonLanguageServer() -{ - using namespace LanguageClient; - QList result; - for (const BaseSettings *setting : LanguageClientManager::currentSettings()) { - if (setting->m_languageFilter.isSupported("foo.py", Constants::C_PY_MIMETYPE)) - result << dynamic_cast(setting); - } - return result; -} - -static PythonLanguageServerState checkPythonLanguageServer(const FilePath &python) -{ - using namespace LanguageClient; - const CommandLine pythonLShelpCommand(python, {"-m", "pylsp", "-h"}); - const FilePath &modulePath = getPylsModulePath(pythonLShelpCommand); - for (const StdIOSettings *serverSetting : configuredPythonLanguageServer()) { - if (modulePath == getPylsModulePath(serverSetting->command())) { - return {serverSetting->m_enabled ? PythonLanguageServerState::AlreadyConfigured - : PythonLanguageServerState::ConfiguredButDisabled, - FilePath()}; - } - } - - QtcProcess pythonProcess; - pythonProcess.setCommand(pythonLShelpCommand); - pythonProcess.runBlocking(); - if (pythonProcess.allOutput().contains("Python Language Server")) - return {PythonLanguageServerState::AlreadyInstalled, modulePath}; - - pythonProcess.setCommand({python, {"-m", "pip", "-V"}}); - pythonProcess.runBlocking(); - if (pythonProcess.allOutput().startsWith("pip ")) - return {PythonLanguageServerState::CanBeInstalled, FilePath()}; - else - return {PythonLanguageServerState::CanNotBeInstalled, FilePath()}; -} - -static FilePath detectPython(const FilePath &documentPath) +FilePath detectPython(const FilePath &documentPath) { FilePath python; @@ -207,292 +78,6 @@ static FilePath detectPython(const FilePath &documentPath) return python; } -PyLSConfigureAssistant *PyLSConfigureAssistant::instance() -{ - static auto *instance = new PyLSConfigureAssistant(PythonPlugin::instance()); - return instance; -} - -const StdIOSettings *PyLSConfigureAssistant::languageServerForPython(const FilePath &python) -{ - return findOrDefault(configuredPythonLanguageServer(), - [pythonModulePath = getPylsModulePath( - CommandLine(python, {"-m", "pylsp"}))](const StdIOSettings *setting) { - return getPylsModulePath(setting->command()) == pythonModulePath; - }); -} - -static Client *registerLanguageServer(const FilePath &python) -{ - auto *settings = new StdIOSettings(); - settings->m_executable = python; - settings->m_arguments = "-m pylsp"; - settings->m_name = PyLSConfigureAssistant::tr("Python Language Server (%1)") - .arg(pythonName(python)); - settings->m_languageFilter.mimeTypes = QStringList(Constants::C_PY_MIMETYPE); - LanguageClientManager::registerClientSettings(settings); - Client *client = LanguageClientManager::clientForSetting(settings).value(0); - PyLSConfigureAssistant::updateEditorInfoBars(python, client); - return client; -} - -class PythonLSInstallHelper : public QObject -{ - Q_OBJECT -public: - PythonLSInstallHelper(const FilePath &python, QPointer document) - : m_python(python) - , m_document(document) - { - m_watcher.setFuture(m_future.future()); - } - - void run() - { - Core::ProgressManager::addTask(m_future.future(), "Install PyLS", installPylsTaskId); - connect(&m_process, - &QtcProcess::finished, - this, - &PythonLSInstallHelper::installFinished); - connect(&m_process, - &QtcProcess::readyReadStandardError, - this, - &PythonLSInstallHelper::errorAvailable); - connect(&m_process, - &QtcProcess::readyReadStandardOutput, - this, - &PythonLSInstallHelper::outputAvailable); - - connect(&m_killTimer, &QTimer::timeout, this, &PythonLSInstallHelper::cancel); - connect(&m_watcher, &QFutureWatcher::canceled, this, &PythonLSInstallHelper::cancel); - - QStringList arguments = {"-m", "pip", "install", "python-lsp-server[all]"}; - - // add --user to global pythons, but skip it for venv pythons - if (!QDir(m_python.parentDir().toString()).exists("activate")) - arguments << "--user"; - - m_process.setCommand({m_python, arguments}); - m_process.start(); - - Core::MessageManager::writeDisrupting( - tr("Running \"%1\" to install Python language server.") - .arg(m_process.commandLine().toUserOutput())); - - m_killTimer.setSingleShot(true); - m_killTimer.start(5 /*minutes*/ * 60 * 1000); - } - -private: - void cancel() - { - m_process.stopProcess(); - Core::MessageManager::writeFlashing( - tr("The Python language server installation was canceled by %1.") - .arg(m_killTimer.isActive() ? tr("user") : tr("time out"))); - } - - void installFinished() - { - m_future.reportFinished(); - if (m_process.result() == QtcProcess::FinishedWithSuccess) { - if (Client *client = registerLanguageServer(m_python)) - LanguageClientManager::openDocumentWithClient(m_document, client); - } else { - Core::MessageManager::writeFlashing( - tr("Installing the Python language server failed with exit code %1") - .arg(m_process.exitCode())); - } - deleteLater(); - } - - void outputAvailable() - { - const QString &stdOut = QString::fromLocal8Bit(m_process.readAllStandardOutput().trimmed()); - if (!stdOut.isEmpty()) - Core::MessageManager::writeSilently(stdOut); - } - - void errorAvailable() - { - const QString &stdErr = QString::fromLocal8Bit(m_process.readAllStandardError().trimmed()); - if (!stdErr.isEmpty()) - Core::MessageManager::writeSilently(stdErr); - } - - QFutureInterface m_future; - QFutureWatcher m_watcher; - QtcProcess m_process; - QTimer m_killTimer; - const FilePath m_python; - QPointer m_document; -}; - -void PyLSConfigureAssistant::installPythonLanguageServer(const FilePath &python, - QPointer document) -{ - document->infoBar()->removeInfo(installPylsInfoBarId); - - // Hide all install info bar entries for this python, but keep them in the list - // so the language server will be setup properly after the installation is done. - for (TextEditor::TextDocument *additionalDocument : m_infoBarEntries[python]) - additionalDocument->infoBar()->removeInfo(installPylsInfoBarId); - - auto install = new PythonLSInstallHelper(python, document); - install->run(); -} - -static void setupPythonLanguageServer(const FilePath &python, - QPointer document) -{ - document->infoBar()->removeInfo(startPylsInfoBarId); - if (Client *client = registerLanguageServer(python)) - LanguageClientManager::openDocumentWithClient(document, client); -} - -static void enablePythonLanguageServer(const FilePath &python, - QPointer document) -{ - document->infoBar()->removeInfo(enablePylsInfoBarId); - if (const StdIOSettings *setting = PyLSConfigureAssistant::languageServerForPython(python)) { - LanguageClientManager::enableClientSettings(setting->m_id); - if (const StdIOSettings *setting = PyLSConfigureAssistant::languageServerForPython(python)) { - if (Client *client = LanguageClientManager::clientForSetting(setting).value(0)) { - LanguageClientManager::openDocumentWithClient(document, client); - PyLSConfigureAssistant::updateEditorInfoBars(python, client); - } - } - } -} - -void PyLSConfigureAssistant::documentOpened(Core::IDocument *document) -{ - auto textDocument = qobject_cast(document); - if (!textDocument || textDocument->mimeType() != Constants::C_PY_MIMETYPE) - return; - - const FilePath &python = detectPython(textDocument->filePath()); - if (!python.exists()) - return; - - instance()->openDocumentWithPython(python, textDocument); -} - -void PyLSConfigureAssistant::openDocumentWithPython(const FilePath &python, - TextEditor::TextDocument *document) -{ - using CheckPylsWatcher = QFutureWatcher; - - QPointer watcher = new CheckPylsWatcher(); - - // cancel and delete watcher after a 10 second timeout - QTimer::singleShot(10000, this, [watcher]() { - if (watcher) { - watcher->cancel(); - watcher->deleteLater(); - } - }); - - connect( - watcher, - &CheckPylsWatcher::resultReadyAt, - this, - [=, document = QPointer(document)]() { - if (!document || !watcher) - return; - handlePyLSState(python, watcher->result(), document); - watcher->deleteLater(); - }); - watcher->setFuture(Utils::runAsync(&checkPythonLanguageServer, python)); -} - -void PyLSConfigureAssistant::handlePyLSState(const FilePath &python, - const PythonLanguageServerState &state, - TextEditor::TextDocument *document) -{ - if (state.state == PythonLanguageServerState::CanNotBeInstalled) - return; - if (state.state == PythonLanguageServerState::AlreadyConfigured) { - if (const StdIOSettings *setting = languageServerForPython(python)) { - if (Client *client = LanguageClientManager::clientForSetting(setting).value(0)) - LanguageClientManager::openDocumentWithClient(document, client); - } - return; - } - - resetEditorInfoBar(document); - Utils::InfoBar *infoBar = document->infoBar(); - if (state.state == PythonLanguageServerState::CanBeInstalled - && infoBar->canInfoBeAdded(installPylsInfoBarId)) { - auto message = tr("Install and set up Python language server (PyLS) for %1 (%2). " - "The language server provides Python specific completion and annotation.") - .arg(pythonName(python), python.toUserOutput()); - Utils::InfoBarEntry info(installPylsInfoBarId, - message, - Utils::InfoBarEntry::GlobalSuppression::Enabled); - info.setCustomButtonInfo(tr("Install"), - [=]() { installPythonLanguageServer(python, document); }); - infoBar->addInfo(info); - m_infoBarEntries[python] << document; - } else if (state.state == PythonLanguageServerState::AlreadyInstalled - && infoBar->canInfoBeAdded(startPylsInfoBarId)) { - auto message = tr("Found a Python language server for %1 (%2). " - "Set it up for this document?") - .arg(pythonName(python), python.toUserOutput()); - Utils::InfoBarEntry info(startPylsInfoBarId, - message, - Utils::InfoBarEntry::GlobalSuppression::Enabled); - info.setCustomButtonInfo(tr("Set Up"), - [=]() { setupPythonLanguageServer(python, document); }); - infoBar->addInfo(info); - m_infoBarEntries[python] << document; - } else if (state.state == PythonLanguageServerState::ConfiguredButDisabled - && infoBar->canInfoBeAdded(enablePylsInfoBarId)) { - auto message = tr("Enable Python language server for %1 (%2)?") - .arg(pythonName(python), python.toUserOutput()); - Utils::InfoBarEntry info(enablePylsInfoBarId, - message, - Utils::InfoBarEntry::GlobalSuppression::Enabled); - info.setCustomButtonInfo(tr("Enable"), - [=]() { enablePythonLanguageServer(python, document); }); - infoBar->addInfo(info); - m_infoBarEntries[python] << document; - } -} - -void PyLSConfigureAssistant::updateEditorInfoBars(const FilePath &python, Client *client) -{ - for (TextEditor::TextDocument *document : instance()->m_infoBarEntries.take(python)) { - instance()->resetEditorInfoBar(document); - if (client) - LanguageClientManager::openDocumentWithClient(document, client); - } -} - -void PyLSConfigureAssistant::resetEditorInfoBar(TextEditor::TextDocument *document) -{ - for (QList &documents : m_infoBarEntries) - documents.removeAll(document); - Utils::InfoBar *infoBar = document->infoBar(); - infoBar->removeInfo(installPylsInfoBarId); - infoBar->removeInfo(startPylsInfoBarId); - infoBar->removeInfo(enablePylsInfoBarId); -} - -PyLSConfigureAssistant::PyLSConfigureAssistant(QObject *parent) - : QObject(parent) -{ - Core::EditorManager::instance(); - - connect(Core::EditorManager::instance(), - &Core::EditorManager::documentClosed, - this, - [this](Core::IDocument *document) { - if (auto textDocument = qobject_cast(document)) - resetEditorInfoBar(textDocument); - }); -} - static QStringList replImportArgs(const FilePath &pythonFile, ReplType type) { using MimeTypes = QList; @@ -543,5 +128,3 @@ void openPythonRepl(const FilePath &file, ReplType type) } // namespace Internal } // namespace Python - -#include "pythonutils.moc" diff --git a/src/plugins/python/pythonutils.h b/src/plugins/python/pythonutils.h index 9bedd9f6cf0..9fa9651b5ff 100644 --- a/src/plugins/python/pythonutils.h +++ b/src/plugins/python/pythonutils.h @@ -27,50 +27,12 @@ #include -#include -#include - -namespace Core { class IDocument; } -namespace LanguageClient { -class Client; -class StdIOSettings; -} -namespace TextEditor { class TextDocument; } - namespace Python { namespace Internal { enum class ReplType { Unmodified, Import, ImportToplevel }; - void openPythonRepl(const Utils::FilePath &file, ReplType type); - -struct PythonLanguageServerState; - -class PyLSConfigureAssistant : public QObject -{ - Q_OBJECT -public: - static PyLSConfigureAssistant *instance(); - - static const LanguageClient::StdIOSettings *languageServerForPython( - const Utils::FilePath &python); - static void documentOpened(Core::IDocument *document); - static void updateEditorInfoBars(const Utils::FilePath &python, LanguageClient::Client *client); - - void openDocumentWithPython(const Utils::FilePath &python, TextEditor::TextDocument *document); - -private: - explicit PyLSConfigureAssistant(QObject *parent); - - void handlePyLSState(const Utils::FilePath &python, - const PythonLanguageServerState &state, - TextEditor::TextDocument *document); - void resetEditorInfoBar(TextEditor::TextDocument *document); - void installPythonLanguageServer(const Utils::FilePath &python, - QPointer document); - - QHash> m_infoBarEntries; -}; +Utils::FilePath detectPython(const Utils::FilePath &documentPath); } // namespace Internal } // namespace Python