diff options
author | David Schulz <david.schulz@qt.io> | 2021-12-13 14:19:30 +0100 |
---|---|---|
committer | David Schulz <david.schulz@qt.io> | 2022-01-18 08:14:57 +0000 |
commit | 49ac087955789e4ab931afe3a34414c7cba68589 (patch) | |
tree | d29c504a53c847130b65e49bae89cd1070ef90ab /src/plugins/python/pythonlanguageclient.cpp | |
parent | 1ba6faeea01cd5eeba854d559344eb01a5c64574 (diff) | |
download | qt-creator-49ac087955789e4ab931afe3a34414c7cba68589.tar.gz |
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 <christian.stenger@qt.io>
Diffstat (limited to 'src/plugins/python/pythonlanguageclient.cpp')
-rw-r--r-- | src/plugins/python/pythonlanguageclient.cpp | 451 |
1 files changed, 451 insertions, 0 deletions
diff --git a/src/plugins/python/pythonlanguageclient.cpp b/src/plugins/python/pythonlanguageclient.cpp new file mode 100644 index 0000000000..a0292f6797 --- /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 <coreplugin/editormanager/editormanager.h> +#include <coreplugin/progressmanager/progressmanager.h> + +#include <languageclient/languageclientmanager.h> + +#include <texteditor/textdocument.h> + +#include <utils/infobar.h> +#include <utils/qtcprocess.h> +#include <utils/runextensions.h> + +#include <QFutureWatcher> +#include <QRegularExpression> +#include <QTimer> + +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<FilePath, QString> 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<FilePath, FilePath> 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<const StdIOSettings *> configuredPythonLanguageServer() +{ + using namespace LanguageClient; + QList<const StdIOSettings *> result; + for (const BaseSettings *setting : LanguageClientManager::currentSettings()) { + if (setting->m_languageFilter.isSupported("foo.py", Constants::C_PY_MIMETYPE)) + result << dynamic_cast<const StdIOSettings *>(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<TextEditor::TextDocument> 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<void>::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<void> m_future; + QFutureWatcher<void> m_watcher; + QtcProcess m_process; + QTimer m_killTimer; + const FilePath m_python; + QPointer<TextEditor::TextDocument> m_document; +}; + +void PyLSConfigureAssistant::installPythonLanguageServer(const FilePath &python, + QPointer<TextEditor::TextDocument> 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<TextEditor::TextDocument> document) +{ + document->infoBar()->removeInfo(startPylsInfoBarId); + if (Client *client = registerLanguageServer(python)) + LanguageClientManager::openDocumentWithClient(document, client); +} + +static void enablePythonLanguageServer(const FilePath &python, + QPointer<TextEditor::TextDocument> 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<TextEditor::TextDocument *>(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<PythonLanguageServerState>; + + QPointer<CheckPylsWatcher> 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<TextEditor::TextDocument>(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<TextEditor::TextDocument *> &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<TextEditor::TextDocument *>(document)) + resetEditorInfoBar(textDocument); + }); +} + +} // namespace Internal +} // namespace Python + +#include "pythonlanguageclient.moc" |