forked from qt-creator/qt-creator
Python: Switch pyls on interpreter change
Change-Id: I458b635986a55003a1e7254e27e2df9667704273 Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
@@ -27,327 +27,22 @@
|
||||
#include "pythonconstants.h"
|
||||
#include "pythonhighlighter.h"
|
||||
#include "pythonindenter.h"
|
||||
#include "pythonplugin.h"
|
||||
#include "pythonproject.h"
|
||||
#include "pythonrunconfiguration.h"
|
||||
#include "pythonsettings.h"
|
||||
|
||||
#include <coreplugin/infobar.h>
|
||||
#include <coreplugin/progressmanager/progressmanager.h>
|
||||
|
||||
#include <languageclient/client.h>
|
||||
#include <languageclient/languageclientinterface.h>
|
||||
#include <languageclient/languageclientmanager.h>
|
||||
|
||||
#include <projectexplorer/session.h>
|
||||
#include <projectexplorer/target.h>
|
||||
#include "pythonutils.h"
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditoractionhandler.h>
|
||||
#include <texteditor/texteditorconstants.h>
|
||||
|
||||
#include <utils/executeondestruction.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <utils/synchronousprocess.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFutureWatcher>
|
||||
#include <QRegularExpression>
|
||||
#include <QTimer>
|
||||
|
||||
using namespace ProjectExplorer;
|
||||
using namespace Utils;
|
||||
|
||||
namespace Python {
|
||||
namespace Internal {
|
||||
|
||||
static constexpr char startPylsInfoBarId[] = "PythonEditor::StartPyls";
|
||||
static constexpr char installPylsInfoBarId[] = "PythonEditor::InstallPyls";
|
||||
static constexpr char installPylsTaskId[] = "PythonEditor::InstallPylsTask";
|
||||
|
||||
struct PythonForProject
|
||||
{
|
||||
FilePath path;
|
||||
PythonProject *project = nullptr;
|
||||
|
||||
QString name() const
|
||||
{
|
||||
if (!path.exists())
|
||||
return {};
|
||||
if (cachedName.first != path) {
|
||||
SynchronousProcess pythonProcess;
|
||||
const CommandLine pythonVersionCommand(path, {"--version"});
|
||||
SynchronousProcessResponse response = pythonProcess.runBlocking(pythonVersionCommand);
|
||||
cachedName.first = path;
|
||||
cachedName.second = response.allOutput().trimmed();
|
||||
}
|
||||
return cachedName.second;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable QPair<FilePath, QString> cachedName;
|
||||
};
|
||||
|
||||
static PythonForProject detectPython(TextEditor::TextDocument *document)
|
||||
{
|
||||
PythonForProject python;
|
||||
|
||||
python.project = qobject_cast<PythonProject *>(
|
||||
SessionManager::projectForFile(document->filePath()));
|
||||
if (!python.project)
|
||||
python.project = qobject_cast<PythonProject *>(SessionManager::startupProject());
|
||||
|
||||
if (python.project) {
|
||||
if (auto target = python.project->activeTarget()) {
|
||||
if (auto runConfig = qobject_cast<PythonRunConfiguration *>(
|
||||
target->activeRunConfiguration())) {
|
||||
python.path = FilePath::fromString(runConfig->interpreter());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!python.path.exists())
|
||||
python.path = PythonSettings::defaultInterpreter().command;
|
||||
|
||||
if (!python.path.exists() && !PythonSettings::interpreters().isEmpty())
|
||||
python.path = PythonSettings::interpreters().first().command;
|
||||
|
||||
return python;
|
||||
}
|
||||
|
||||
FilePath getPylsModulePath(CommandLine pylsCommand)
|
||||
{
|
||||
pylsCommand.addArg("-h");
|
||||
SynchronousProcess pythonProcess;
|
||||
pythonProcess.setEnvironment(pythonProcess.environment() + QStringList("PYTHONVERBOSE=x"));
|
||||
SynchronousProcessResponse response = pythonProcess.runBlocking(pylsCommand);
|
||||
|
||||
static const QString pylsInitPattern = "(.*)"
|
||||
+ QRegularExpression::escape(
|
||||
QDir::toNativeSeparators("/pyls/__init__.py"))
|
||||
+ '$';
|
||||
static const QRegularExpression regexCached(" matches " + pylsInitPattern,
|
||||
QRegularExpression::MultilineOption);
|
||||
static const QRegularExpression regexNotCached(" code object from " + pylsInitPattern,
|
||||
QRegularExpression::MultilineOption);
|
||||
|
||||
const QString &output = response.allOutput();
|
||||
for (auto regex : {regexCached, regexNotCached}) {
|
||||
QRegularExpressionMatch result = regex.match(output);
|
||||
if (result.hasMatch())
|
||||
return FilePath::fromUserInput(result.captured(1));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct PythonLanguageServerState
|
||||
{
|
||||
enum { CanNotBeInstalled, CanBeInstalled, AlreadyInstalled, AlreadyConfigured } state;
|
||||
FilePath pylsModulePath;
|
||||
};
|
||||
|
||||
static QList<const LanguageClient::StdIOSettings *> configuredPythonLanguageServer(
|
||||
Core::IDocument *doc)
|
||||
{
|
||||
using namespace LanguageClient;
|
||||
QList<const StdIOSettings *> result;
|
||||
for (const BaseSettings *setting : LanguageClientManager::currentSettings()) {
|
||||
if (setting->m_languageFilter.isSupported(doc))
|
||||
result << dynamic_cast<const StdIOSettings *>(setting);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static PythonLanguageServerState checkPythonLanguageServer(const FilePath &python,
|
||||
TextEditor::TextDocument *document)
|
||||
{
|
||||
using namespace LanguageClient;
|
||||
SynchronousProcess pythonProcess;
|
||||
const CommandLine pythonLShelpCommand(python, {"-m", "pyls", "-h"});
|
||||
SynchronousProcessResponse response = pythonProcess.runBlocking(pythonLShelpCommand);
|
||||
if (response.allOutput().contains("Python Language Server")) {
|
||||
const FilePath &modulePath = getPylsModulePath(pythonLShelpCommand);
|
||||
for (const StdIOSettings *serverSetting : configuredPythonLanguageServer(document)) {
|
||||
CommandLine serverCommand(FilePath::fromUserInput(serverSetting->m_executable),
|
||||
serverSetting->arguments(),
|
||||
CommandLine::Raw);
|
||||
|
||||
if (modulePath == getPylsModulePath(serverCommand))
|
||||
return {PythonLanguageServerState::AlreadyConfigured, FilePath()};
|
||||
}
|
||||
|
||||
return {PythonLanguageServerState::AlreadyInstalled, getPylsModulePath(pythonLShelpCommand)};
|
||||
}
|
||||
|
||||
const CommandLine pythonPipVersionCommand(python, {"-m", "pip", "-V"});
|
||||
response = pythonProcess.runBlocking(pythonPipVersionCommand);
|
||||
if (response.allOutput().startsWith("pip "))
|
||||
return {PythonLanguageServerState::CanBeInstalled, FilePath()};
|
||||
else
|
||||
return {PythonLanguageServerState::CanNotBeInstalled, FilePath()};
|
||||
}
|
||||
|
||||
static LanguageClient::Client *registerLanguageServer(const PythonForProject &python)
|
||||
{
|
||||
auto *settings = new LanguageClient::StdIOSettings();
|
||||
settings->m_executable = python.path.toString();
|
||||
settings->m_arguments = "-m pyls";
|
||||
settings->m_name = PythonEditorFactory::tr("Python Language Server (%1)").arg(python.name());
|
||||
settings->m_languageFilter.mimeTypes = QStringList(Constants::C_PY_MIMETYPE);
|
||||
LanguageClient::LanguageClientManager::registerClientSettings(settings);
|
||||
return LanguageClient::LanguageClientManager::clientForSetting(settings).value(0);
|
||||
}
|
||||
|
||||
class PythonLSInstallHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
PythonLSInstallHelper(const PythonForProject &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,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this,
|
||||
&PythonLSInstallHelper::installFinished);
|
||||
connect(&m_process,
|
||||
&QProcess::readyReadStandardError,
|
||||
this,
|
||||
&PythonLSInstallHelper::errorAvailable);
|
||||
connect(&m_process,
|
||||
&QProcess::readyReadStandardOutput,
|
||||
this,
|
||||
&PythonLSInstallHelper::outputAvailable);
|
||||
|
||||
connect(&m_killTimer, &QTimer::timeout, this, &PythonLSInstallHelper::cancel);
|
||||
connect(&m_watcher, &QFutureWatcher<void>::canceled, this, &PythonLSInstallHelper::cancel);
|
||||
|
||||
// on windows the pyls 0.28.3 crashes with pylint so just install the pyflakes linter
|
||||
const QString &pylsVersion = HostOsInfo::isWindowsHost()
|
||||
? QString{"python-language-server[pyflakes]"}
|
||||
: QString{"python-language-server[all]"};
|
||||
|
||||
m_process.start(m_python.path.toString(),
|
||||
{"-m", "pip", "install", pylsVersion});
|
||||
|
||||
Core::MessageManager::write(tr("Running '%1 %2' to install python language server")
|
||||
.arg(m_process.program(), m_process.arguments().join(' ')));
|
||||
|
||||
m_killTimer.setSingleShot(true);
|
||||
m_killTimer.start(5 /*minutes*/ * 60 * 1000);
|
||||
}
|
||||
|
||||
private:
|
||||
void cancel()
|
||||
{
|
||||
SynchronousProcess::stopProcess(m_process);
|
||||
Core::MessageManager::write(
|
||||
tr("The Python language server installation canceled by %1.")
|
||||
.arg(m_killTimer.isActive() ? tr("user") : tr("time out")));
|
||||
}
|
||||
|
||||
void installFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
||||
{
|
||||
m_future.reportFinished();
|
||||
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
|
||||
if (LanguageClient::Client *client = registerLanguageServer(m_python))
|
||||
LanguageClient::LanguageClientManager::reOpenDocumentWithClient(m_document, client);
|
||||
} else {
|
||||
Core::MessageManager::write(
|
||||
tr("Installing the Python language server failed with exit code %1").arg(exitCode));
|
||||
}
|
||||
deleteLater();
|
||||
}
|
||||
void outputAvailable()
|
||||
{
|
||||
const QString &stdOut = QString::fromLocal8Bit(m_process.readAllStandardOutput().trimmed());
|
||||
if (!stdOut.isEmpty())
|
||||
Core::MessageManager::write(stdOut);
|
||||
}
|
||||
|
||||
void errorAvailable()
|
||||
{
|
||||
const QString &stdErr = QString::fromLocal8Bit(m_process.readAllStandardError().trimmed());
|
||||
if (!stdErr.isEmpty())
|
||||
Core::MessageManager::write(stdErr);
|
||||
}
|
||||
|
||||
QFutureInterface<void> m_future;
|
||||
QFutureWatcher<void> m_watcher;
|
||||
QProcess m_process;
|
||||
QTimer m_killTimer;
|
||||
const PythonForProject m_python;
|
||||
QPointer<TextEditor::TextDocument> m_document;
|
||||
};
|
||||
|
||||
static void installPythonLanguageServer(const PythonForProject &python,
|
||||
QPointer<TextEditor::TextDocument> document)
|
||||
{
|
||||
document->infoBar()->removeInfo(installPylsInfoBarId);
|
||||
|
||||
auto install = new PythonLSInstallHelper(python, document);
|
||||
install->run();
|
||||
}
|
||||
|
||||
static void setupPythonLanguageServer(const PythonForProject &python,
|
||||
QPointer<TextEditor::TextDocument> document)
|
||||
{
|
||||
document->infoBar()->removeInfo(startPylsInfoBarId);
|
||||
if (LanguageClient::Client *client = registerLanguageServer(python))
|
||||
LanguageClient::LanguageClientManager::reOpenDocumentWithClient(document, client);
|
||||
}
|
||||
|
||||
static void updateEditorInfoBar(const PythonForProject &python, TextEditor::TextDocument *document)
|
||||
{
|
||||
const PythonLanguageServerState &lsState = checkPythonLanguageServer(python.path, document);
|
||||
|
||||
if (lsState.state == PythonLanguageServerState::CanNotBeInstalled
|
||||
|| lsState.state == PythonLanguageServerState::AlreadyConfigured) {
|
||||
return;
|
||||
}
|
||||
|
||||
Core::InfoBar *infoBar = document->infoBar();
|
||||
if (lsState.state == PythonLanguageServerState::CanBeInstalled
|
||||
&& infoBar->canInfoBeAdded(installPylsInfoBarId)) {
|
||||
auto message
|
||||
= PythonEditorFactory::tr(
|
||||
"Install and set up Python language server (PyLS) for %1 (%2). "
|
||||
"The language server provides Python specific completions and annotations.")
|
||||
.arg(python.name(), python.path.toUserOutput());
|
||||
Core::InfoBarEntry info(installPylsInfoBarId,
|
||||
message,
|
||||
Core::InfoBarEntry::GlobalSuppression::Enabled);
|
||||
info.setCustomButtonInfo(TextEditor::BaseTextEditor::tr("Install"),
|
||||
[=]() { installPythonLanguageServer(python, document); });
|
||||
infoBar->addInfo(info);
|
||||
} else if (lsState.state == PythonLanguageServerState::AlreadyInstalled
|
||||
&& infoBar->canInfoBeAdded(startPylsInfoBarId)) {
|
||||
auto message = PythonEditorFactory::tr("Found a Python language server for %1 (%2). "
|
||||
"Should this one be set up for this document?")
|
||||
.arg(python.name(), python.path.toUserOutput());
|
||||
Core::InfoBarEntry info(startPylsInfoBarId,
|
||||
message,
|
||||
Core::InfoBarEntry::GlobalSuppression::Enabled);
|
||||
info.setCustomButtonInfo(TextEditor::BaseTextEditor::tr("Setup"),
|
||||
[=]() { setupPythonLanguageServer(python, document); });
|
||||
infoBar->addInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
static void documentOpened(Core::IDocument *document)
|
||||
{
|
||||
auto textDocument = qobject_cast<TextEditor::TextDocument *>(document);
|
||||
if (!textDocument || textDocument->mimeType() != Constants::C_PY_MIMETYPE)
|
||||
return;
|
||||
|
||||
const PythonForProject &python = detectPython(textDocument);
|
||||
if (!python.path.exists())
|
||||
const Utils::FilePath &python = detectPython(textDocument->filePath());
|
||||
if (!python.exists())
|
||||
return;
|
||||
|
||||
updateEditorInfoBar(python, textDocument);
|
||||
@@ -368,7 +63,7 @@ PythonEditorFactory::PythonEditorFactory()
|
||||
setDocumentCreator([] { return new TextEditor::TextDocument(Constants::C_PYTHONEDITOR_ID); });
|
||||
setIndenterCreator([](QTextDocument *doc) { return new PythonIndenter(doc); });
|
||||
setSyntaxHighlighterCreator([] { return new PythonHighlighter; });
|
||||
setCommentDefinition(CommentDefinition::HashStyle);
|
||||
setCommentDefinition(Utils::CommentDefinition::HashStyle);
|
||||
setParenthesesMatchingEnabled(true);
|
||||
setCodeFoldingSupported(true);
|
||||
|
||||
@@ -378,5 +73,3 @@ PythonEditorFactory::PythonEditorFactory()
|
||||
|
||||
} // namespace Internal
|
||||
} // namespace Python
|
||||
|
||||
#include "pythoneditor.moc"
|
||||
|
||||
Reference in New Issue
Block a user