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/pythonutils.cpp | |
| parent | 1ba6faeea01cd5eeba854d559344eb01a5c64574 (diff) | |
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/pythonutils.cpp')
| -rw-r--r-- | src/plugins/python/pythonutils.cpp | 425 |
1 files changed, 4 insertions, 421 deletions
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 <coreplugin/editormanager/editormanager.h> -#include <coreplugin/progressmanager/progressmanager.h> - -#include <languageclient/languageclientmanager.h> -#include <languageclient/languageclientsettings.h> +#include <coreplugin/messagemanager.h> +#include <projectexplorer/project.h> #include <projectexplorer/session.h> #include <projectexplorer/target.h> -#include <texteditor/textdocument.h> - +#include <utils/algorithm.h> #include <utils/consoleprocess.h> -#include <utils/infobar.h> #include <utils/mimetypes/mimedatabase.h> -#include <utils/qtcassert.h> -#include <utils/qtcprocess.h> -#include <utils/runextensions.h> -#include <QDir> -#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()}; -} - -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<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); - }); -} - static QStringList replImportArgs(const FilePath &pythonFile, ReplType type) { using MimeTypes = QList<MimeType>; @@ -543,5 +128,3 @@ void openPythonRepl(const FilePath &file, ReplType type) } // namespace Internal } // namespace Python - -#include "pythonutils.moc" |
