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>
This commit is contained in:
David Schulz
2021-12-13 14:19:30 +01:00
parent 1ba6faeea0
commit 49ac087955
9 changed files with 547 additions and 477 deletions

View File

@@ -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
)

View File

@@ -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

View File

@@ -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",

View File

@@ -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 &regex : {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"

View File

@@ -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 <utils/fileutils.h>
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<TextEditor::TextDocument> document);
QHash<Utils::FilePath, QList<TextEditor::TextDocument *>> m_infoBarEntries;
};
} // namespace Internal
} // namespace Python

View File

@@ -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 <coreplugin/fileiconprovider.h>

View File

@@ -26,9 +26,9 @@
#include "pythonrunconfiguration.h"
#include "pythonconstants.h"
#include "pythonlanguageclient.h"
#include "pythonproject.h"
#include "pythonsettings.h"
#include "pythonutils.h"
#include <coreplugin/icore.h>
#include <coreplugin/editormanager/editormanager.h>

View File

@@ -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 &regex : {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"

View File

@@ -27,50 +27,12 @@
#include <utils/fileutils.h>
#include <QHash>
#include <QObject>
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<TextEditor::TextDocument> document);
QHash<Utils::FilePath, QList<TextEditor::TextDocument *>> m_infoBarEntries;
};
Utils::FilePath detectPython(const Utils::FilePath &documentPath);
} // namespace Internal
} // namespace Python