summaryrefslogtreecommitdiff
path: root/src/plugins/python
diff options
context:
space:
mode:
authorDavid Schulz <david.schulz@qt.io>2019-10-01 13:16:17 +0200
committerDavid Schulz <david.schulz@qt.io>2019-10-18 05:23:10 +0000
commitc8ccfea225d506513e5575dd100bf6175720e29a (patch)
tree784c7ef8f80db48e0660e56a8ac29a3ba41e5518 /src/plugins/python
parent5b2ca680a7c26a476e92582932aca016a11374b1 (diff)
downloadqt-creator-c8ccfea225d506513e5575dd100bf6175720e29a.tar.gz
Python: Switch pyls on interpreter change
Change-Id: I458b635986a55003a1e7254e27e2df9667704273 Reviewed-by: Christian Stenger <christian.stenger@qt.io>
Diffstat (limited to 'src/plugins/python')
-rw-r--r--src/plugins/python/CMakeLists.txt1
-rw-r--r--src/plugins/python/python.pro6
-rw-r--r--src/plugins/python/python.qbs2
-rw-r--r--src/plugins/python/pythoneditor.cpp315
-rw-r--r--src/plugins/python/pythonrunconfiguration.cpp37
-rw-r--r--src/plugins/python/pythonrunconfiguration.h1
-rw-r--r--src/plugins/python/pythonutils.cpp353
-rw-r--r--src/plugins/python/pythonutils.h43
8 files changed, 444 insertions, 314 deletions
diff --git a/src/plugins/python/CMakeLists.txt b/src/plugins/python/CMakeLists.txt
index 27f45669f7..7ce8b21e06 100644
--- a/src/plugins/python/CMakeLists.txt
+++ b/src/plugins/python/CMakeLists.txt
@@ -12,4 +12,5 @@ add_qtc_plugin(Python
pythonrunconfiguration.cpp pythonrunconfiguration.h
pythonsettings.cpp pythonsettings.h
pythonscanner.cpp pythonscanner.h
+ pythonutils.cpp pythonutils.h
)
diff --git a/src/plugins/python/python.pro b/src/plugins/python/python.pro
index ec4ea74c06..29c53be779 100644
--- a/src/plugins/python/python.pro
+++ b/src/plugins/python/python.pro
@@ -13,7 +13,8 @@ HEADERS += \
pythonproject.h \
pythonrunconfiguration.h \
pythonscanner.h \
- pythonsettings.h
+ pythonsettings.h \
+ pythonutils.h
SOURCES += \
pythonplugin.cpp \
@@ -23,7 +24,8 @@ SOURCES += \
pythonproject.cpp \
pythonrunconfiguration.cpp \
pythonscanner.cpp \
- pythonsettings.cpp
+ pythonsettings.cpp \
+ pythonutils.cpp
RESOURCES += \
python.qrc
diff --git a/src/plugins/python/python.qbs b/src/plugins/python/python.qbs
index 552186f8c9..9fd37a4777 100644
--- a/src/plugins/python/python.qbs
+++ b/src/plugins/python/python.qbs
@@ -33,6 +33,8 @@ QtcPlugin {
"pythonscanner.cpp",
"pythonsettings.cpp",
"pythonsettings.h",
+ "pythonutils.cpp",
+ "pythonutils.h",
]
}
}
diff --git a/src/plugins/python/pythoneditor.cpp b/src/plugins/python/pythoneditor.cpp
index 8422eb221f..6481897e69 100644
--- a/src/plugins/python/pythoneditor.cpp
+++ b/src/plugins/python/pythoneditor.cpp
@@ -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"
diff --git a/src/plugins/python/pythonrunconfiguration.cpp b/src/plugins/python/pythonrunconfiguration.cpp
index bb61f275ae..53245b1632 100644
--- a/src/plugins/python/pythonrunconfiguration.cpp
+++ b/src/plugins/python/pythonrunconfiguration.cpp
@@ -23,20 +23,26 @@
**
****************************************************************************/
+#include "pythonrunconfiguration.h"
+
#include "pythonconstants.h"
#include "pythonproject.h"
-#include "pythonrunconfiguration.h"
#include "pythonsettings.h"
+#include "pythonutils.h"
#include <coreplugin/icore.h>
#include <coreplugin/editormanager/editormanager.h>
+#include <languageclient/languageclientmanager.h>
+
#include <projectexplorer/localenvironmentaspect.h>
#include <projectexplorer/projectconfigurationaspects.h>
#include <projectexplorer/runconfigurationaspects.h>
#include <projectexplorer/target.h>
#include <projectexplorer/taskhub.h>
+#include <texteditor/textdocument.h>
+
#include <utils/fileutils.h>
#include <utils/outputformatter.h>
#include <utils/theme/theme.h>
@@ -248,6 +254,8 @@ PythonRunConfiguration::PythonRunConfiguration(Target *target, Core::Id id)
{
auto interpreterAspect = addAspect<InterpreterAspect>();
interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter");
+ connect(interpreterAspect, &InterpreterAspect::changed,
+ this, &PythonRunConfiguration::updateLanguageServer);
connect(PythonSettings::instance(), &PythonSettings::interpretersChanged,
interpreterAspect, &InterpreterAspect::updateInterpreters);
@@ -283,6 +291,33 @@ void PythonRunConfiguration::doAdditionalSetup(const RunConfigurationCreationInf
updateTargetInformation();
}
+void PythonRunConfiguration::updateLanguageServer()
+{
+ using namespace LanguageClient;
+
+ const FilePath python(FilePath::fromUserInput(interpreter()));
+
+ if (const StdIOSettings *lsSetting = languageServerForPython(python)) {
+ if (Client *client = LanguageClientManager::clientForSetting(lsSetting).value(0)) {
+ for (FilePath &file : project()->files(Project::AllFiles)) {
+ if (auto document = TextEditor::TextDocument::textDocumentForFilePath(file)) {
+ if (document->mimeType() == Constants::C_PY_MIMETYPE) {
+ resetEditorInfoBar(document);
+ LanguageClientManager::reOpenDocumentWithClient(document, client);
+ }
+ }
+ }
+ }
+ }
+
+ for (FilePath &file : project()->files(Project::AllFiles)) {
+ if (auto document = TextEditor::TextDocument::textDocumentForFilePath(file)) {
+ if (document->mimeType() == Constants::C_PY_MIMETYPE)
+ updateEditorInfoBar(python, document);
+ }
+ }
+}
+
bool PythonRunConfiguration::supportsDebugger() const
{
return true;
diff --git a/src/plugins/python/pythonrunconfiguration.h b/src/plugins/python/pythonrunconfiguration.h
index bb5139e615..6d5a13683a 100644
--- a/src/plugins/python/pythonrunconfiguration.h
+++ b/src/plugins/python/pythonrunconfiguration.h
@@ -46,6 +46,7 @@ public:
private:
void doAdditionalSetup(const ProjectExplorer::RunConfigurationCreationInfo &) final;
+ void updateLanguageServer();
bool supportsDebugger() const;
QString mainScript() const;
diff --git a/src/plugins/python/pythonutils.cpp b/src/plugins/python/pythonutils.cpp
new file mode 100644
index 0000000000..29fc14953b
--- /dev/null
+++ b/src/plugins/python/pythonutils.cpp
@@ -0,0 +1,353 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 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 "pythonutils.h"
+
+#include "pythonconstants.h"
+#include "pythonproject.h"
+#include "pythonrunconfiguration.h"
+#include "pythonsettings.h"
+
+#include <coreplugin/infobar.h>
+#include <coreplugin/progressmanager/progressmanager.h>
+
+#include <languageclient/languageclientsettings.h>
+#include <languageclient/languageclientmanager.h>
+
+#include <projectexplorer/session.h>
+#include <projectexplorer/target.h>
+
+#include <texteditor/textdocument.h>
+
+#include <utils/synchronousprocess.h>
+
+#include <QDir>
+#include <QFutureWatcher>
+#include <QRegularExpression>
+#include <QTimer>
+
+using namespace Utils;
+
+namespace Python {
+namespace Internal {
+
+static constexpr char startPylsInfoBarId[] = "Python::StartPyls";
+static constexpr char installPylsInfoBarId[] = "Python::InstallPyls";
+static constexpr char installPylsTaskId[] = "Python::InstallPylsTask";
+static constexpr char pythonUtilsTrContext[] = "Python::Utils";
+
+struct PythonLanguageServerState
+{
+ enum { CanNotBeInstalled, CanBeInstalled, AlreadyInstalled, AlreadyConfigured } 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()) {
+ SynchronousProcess pythonProcess;
+ pythonProcess.setTimeoutS(2);
+ const CommandLine pythonVersionCommand(pythonPath, {"--version"});
+ const SynchronousProcessResponse response = pythonProcess.runBlocking(pythonVersionCommand);
+ if (response.result != SynchronousProcessResponse::Finished)
+ return {};
+ name = response.allOutput().trimmed();
+ nameForPython[pythonPath] = name;
+ }
+ return name;
+}
+
+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 {};
+}
+
+QList<const LanguageClient::StdIOSettings *> configuredPythonLanguageServer()
+{
+ using namespace LanguageClient;
+ QList<const StdIOSettings *> result;
+ for (const BaseSettings *setting : LanguageClientManager::currentSettings()) {
+ if (setting->m_languageFilter.isSupported(FilePath::fromString("foo.py"),
+ Constants::C_PY_MIMETYPE)) {
+ result << dynamic_cast<const StdIOSettings *>(setting);
+ }
+ }
+ return result;
+}
+
+static PythonLanguageServerState checkPythonLanguageServer(const FilePath &python)
+{
+ 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()) {
+ if (modulePath == getPylsModulePath(serverSetting->command()))
+ 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()};
+}
+
+FilePath detectPython(const FilePath &documentPath)
+{
+ FilePath python;
+
+ PythonProject *project = qobject_cast<PythonProject *>(
+ ProjectExplorer::SessionManager::projectForFile(documentPath));
+ if (!project)
+ project = qobject_cast<PythonProject *>(ProjectExplorer::SessionManager::startupProject());
+
+ if (project) {
+ if (auto target = project->activeTarget()) {
+ if (auto runConfig = qobject_cast<PythonRunConfiguration *>(
+ target->activeRunConfiguration())) {
+ python = FilePath::fromString(runConfig->interpreter());
+ }
+ }
+ }
+
+ if (!python.exists())
+ python = PythonSettings::defaultInterpreter().command;
+
+ if (!python.exists() && !PythonSettings::interpreters().isEmpty())
+ python = PythonSettings::interpreters().first().command;
+
+ return python;
+}
+
+const LanguageClient::StdIOSettings *languageServerForPython(const FilePath &python)
+{
+ return findOrDefault(configuredPythonLanguageServer(),
+ [pythonModulePath = getPylsModulePath(CommandLine(python, {"-m", "pyls"}))](
+ const LanguageClient::StdIOSettings *setting) {
+ return getPylsModulePath(setting->command()) == pythonModulePath;
+ });
+}
+
+static LanguageClient::Client *registerLanguageServer(const FilePath &python)
+{
+ auto *settings = new LanguageClient::StdIOSettings();
+ settings->m_executable = python.toString();
+ settings->m_arguments = "-m pyls";
+ settings->m_name = QCoreApplication::translate(pythonUtilsTrContext,
+ "Python Language Server (%1)")
+ .arg(pythonName(python));
+ 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 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,
+ 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.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 FilePath m_python;
+ QPointer<TextEditor::TextDocument> m_document;
+};
+
+static void installPythonLanguageServer(const FilePath &python,
+ QPointer<TextEditor::TextDocument> document)
+{
+ document->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 (LanguageClient::Client *client = registerLanguageServer(python))
+ LanguageClient::LanguageClientManager::reOpenDocumentWithClient(document, client);
+}
+
+void updateEditorInfoBar(const FilePath &python, TextEditor::TextDocument *document)
+{
+ const PythonLanguageServerState &lsState = checkPythonLanguageServer(python);
+
+ if (lsState.state == PythonLanguageServerState::CanNotBeInstalled
+ || lsState.state == PythonLanguageServerState::AlreadyConfigured) {
+ return;
+ }
+
+ Core::InfoBar *infoBar = document->infoBar();
+ infoBar->removeInfo(installPylsInfoBarId);
+ infoBar->removeInfo(startPylsInfoBarId);
+ if (lsState.state == PythonLanguageServerState::CanBeInstalled
+ && infoBar->canInfoBeAdded(installPylsInfoBarId)) {
+ auto message
+ = QCoreApplication::translate(pythonUtilsTrContext,
+ "Install and set up Python language server (PyLS) for %1 (%2). "
+ "The language server provides Python specific completions and annotations.")
+ .arg(pythonName(python), python.toUserOutput());
+ Core::InfoBarEntry info(installPylsInfoBarId,
+ message,
+ Core::InfoBarEntry::GlobalSuppression::Enabled);
+ info.setCustomButtonInfo(QCoreApplication::translate(pythonUtilsTrContext, "Install"),
+ [=]() { installPythonLanguageServer(python, document); });
+ infoBar->addInfo(info);
+ } else if (lsState.state == PythonLanguageServerState::AlreadyInstalled
+ && infoBar->canInfoBeAdded(startPylsInfoBarId)) {
+ auto message = QCoreApplication::translate(pythonUtilsTrContext,
+ "Found a Python language server for %1 (%2). "
+ "Should this one be set up for this document?")
+ .arg(pythonName(python), python.toUserOutput());
+ Core::InfoBarEntry info(startPylsInfoBarId,
+ message,
+ Core::InfoBarEntry::GlobalSuppression::Enabled);
+ info.setCustomButtonInfo(QCoreApplication::translate(pythonUtilsTrContext, "Setup"),
+ [=]() { setupPythonLanguageServer(python, document); });
+ infoBar->addInfo(info);
+ }
+}
+
+void resetEditorInfoBar(TextEditor::TextDocument *document)
+{
+ Core::InfoBar *infoBar = document->infoBar();
+ infoBar->removeInfo(installPylsInfoBarId);
+ infoBar->removeInfo(startPylsInfoBarId);
+}
+
+} // namespace Internal
+} // namespace Python
+
+#include "pythonutils.moc"
+
diff --git a/src/plugins/python/pythonutils.h b/src/plugins/python/pythonutils.h
new file mode 100644
index 0000000000..f376a4d749
--- /dev/null
+++ b/src/plugins/python/pythonutils.h
@@ -0,0 +1,43 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 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 <utils/fileutils.h>
+
+#pragma once
+
+namespace TextEditor { class TextDocument; }
+namespace LanguageClient { class StdIOSettings; }
+
+namespace Python {
+namespace Internal {
+
+QList<const LanguageClient::StdIOSettings *> configuredPythonLanguageServers();
+const LanguageClient::StdIOSettings *languageServerForPython(const Utils::FilePath &python);
+Utils::FilePath detectPython(const Utils::FilePath &NdocumentPath);
+void updateEditorInfoBar(const Utils::FilePath &python, TextEditor::TextDocument *document);
+void resetEditorInfoBar(TextEditor::TextDocument *document);
+
+} // namespace Internal
+} // namespace Python