diff options
Diffstat (limited to 'src/plugins/python')
-rw-r--r-- | src/plugins/python/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/plugins/python/python.pro | 6 | ||||
-rw-r--r-- | src/plugins/python/python.qbs | 2 | ||||
-rw-r--r-- | src/plugins/python/pythoneditor.cpp | 315 | ||||
-rw-r--r-- | src/plugins/python/pythonrunconfiguration.cpp | 37 | ||||
-rw-r--r-- | src/plugins/python/pythonrunconfiguration.h | 1 | ||||
-rw-r--r-- | src/plugins/python/pythonutils.cpp | 353 | ||||
-rw-r--r-- | src/plugins/python/pythonutils.h | 43 |
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 |