From e232dfe2e67f5513d841716130bb5fe4ac720028 Mon Sep 17 00:00:00 2001 From: David Schulz Date: Wed, 24 Jul 2019 10:19:19 +0200 Subject: [PATCH] Python: Check for Python language server after document was opened Show an info bar entry with a one click solution to setup a language server if the python which is most likely to be used for this file has an installed language server. Change-Id: Ia52bb043b543699527740951f68cc6be546833df Reviewed-by: Christian Stenger --- src/plugins/python/CMakeLists.txt | 2 +- src/plugins/python/python.qbs | 1 + src/plugins/python/python_dependencies.pri | 5 +- src/plugins/python/pythoneditor.cpp | 223 +++++++++++++++++++- src/plugins/python/pythonrunconfiguration.h | 2 +- 5 files changed, 218 insertions(+), 15 deletions(-) diff --git a/src/plugins/python/CMakeLists.txt b/src/plugins/python/CMakeLists.txt index 7ecc160f3a9..27f45669f7f 100644 --- a/src/plugins/python/CMakeLists.txt +++ b/src/plugins/python/CMakeLists.txt @@ -1,5 +1,5 @@ add_qtc_plugin(Python - PLUGIN_DEPENDS Core ProjectExplorer TextEditor + PLUGIN_DEPENDS Core LanguageClient ProjectExplorer TextEditor SOURCES python.qrc pythoneditor.cpp pythoneditor.h diff --git a/src/plugins/python/python.qbs b/src/plugins/python/python.qbs index c90f76d5d1d..552186f8c99 100644 --- a/src/plugins/python/python.qbs +++ b/src/plugins/python/python.qbs @@ -9,6 +9,7 @@ QtcPlugin { Depends { name: "Core" } Depends { name: "TextEditor" } Depends { name: "ProjectExplorer" } + Depends { name: "LanguageClient" } Group { name: "General" diff --git a/src/plugins/python/python_dependencies.pri b/src/plugins/python/python_dependencies.pri index 5f2a87c2830..a62bb0e8af8 100644 --- a/src/plugins/python/python_dependencies.pri +++ b/src/plugins/python/python_dependencies.pri @@ -4,5 +4,6 @@ QTC_LIB_DEPENDS += \ utils QTC_PLUGIN_DEPENDS += \ coreplugin \ - texteditor \ - projectexplorer + languageclient \ + projectexplorer \ + texteditor diff --git a/src/plugins/python/pythoneditor.cpp b/src/plugins/python/pythoneditor.cpp index 145e88e1371..434ec9e1cc1 100644 --- a/src/plugins/python/pythoneditor.cpp +++ b/src/plugins/python/pythoneditor.cpp @@ -25,40 +25,241 @@ #include "pythoneditor.h" #include "pythonconstants.h" -#include "pythonplugin.h" -#include "pythonindenter.h" #include "pythonhighlighter.h" +#include "pythonindenter.h" +#include "pythonplugin.h" +#include "pythonproject.h" +#include "pythonrunconfiguration.h" +#include "pythonsettings.h" +#include + +#include +#include +#include + +#include +#include + +#include #include #include -#include #include +#include #include +#include -using namespace TextEditor; +using namespace ProjectExplorer; +using namespace Utils; namespace Python { namespace Internal { +static constexpr char startPylsInfoBarId[] = "PythonEditor::StartPyls"; + +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 cachedName; +}; + +static PythonForProject detectPython(TextEditor::TextDocument *document) +{ + PythonForProject python; + + python.project = qobject_cast( + SessionManager::projectForFile(document->filePath())); + if (!python.project) + python.project = qobject_cast(SessionManager::startupProject()); + + if (python.project) { + if (auto target = python.project->activeTarget()) { + if (auto runConfig = qobject_cast( + 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 QString cachedPattern = " matches " + pylsInitPattern; + 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 configuredPythonLanguageServer( + Core::IDocument *doc) +{ + using namespace LanguageClient; + QList result; + for (const BaseSettings *setting : LanguageClientManager::currentSettings()) { + if (setting->m_languageFilter.isSupported(doc)) + result << dynamic_cast(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); +} + +static void setupPythonLanguageServer(const PythonForProject &python, + QPointer 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 + || lsState.state == PythonLanguageServerState::CanBeInstalled /* TODO */) { + return; + } + + Core::InfoBar *infoBar = document->infoBar(); + 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(document); + if (!textDocument || textDocument->mimeType() != Constants::C_PY_MIMETYPE) + return; + + const PythonForProject &python = detectPython(textDocument); + if (!python.path.exists()) + return; + + updateEditorInfoBar(python, textDocument); +} + PythonEditorFactory::PythonEditorFactory() { setId(Constants::C_PYTHONEDITOR_ID); - setDisplayName(QCoreApplication::translate("OpenWith::Editors", Constants::C_EDITOR_DISPLAY_NAME)); + setDisplayName( + QCoreApplication::translate("OpenWith::Editors", Constants::C_EDITOR_DISPLAY_NAME)); addMimeType(Constants::C_PY_MIMETYPE); - setEditorActionHandlers(TextEditorActionHandler::Format - | TextEditorActionHandler::UnCommentSelection - | TextEditorActionHandler::UnCollapseAll - | TextEditorActionHandler::FollowSymbolUnderCursor); + setEditorActionHandlers(TextEditor::TextEditorActionHandler::Format + | TextEditor::TextEditorActionHandler::UnCommentSelection + | TextEditor::TextEditorActionHandler::UnCollapseAll + | TextEditor::TextEditorActionHandler::FollowSymbolUnderCursor); - setDocumentCreator([] { return new TextDocument(Constants::C_PYTHONEDITOR_ID); }); + setDocumentCreator([] { return new TextEditor::TextDocument(Constants::C_PYTHONEDITOR_ID); }); setIndenterCreator([](QTextDocument *doc) { return new PythonIndenter(doc); }); setSyntaxHighlighterCreator([] { return new PythonHighlighter; }); - setCommentDefinition(Utils::CommentDefinition::HashStyle); + setCommentDefinition(CommentDefinition::HashStyle); setParenthesesMatchingEnabled(true); setCodeFoldingSupported(true); + + connect(Core::EditorManager::instance(), &Core::EditorManager::documentOpened, + this, documentOpened); } } // namespace Internal diff --git a/src/plugins/python/pythonrunconfiguration.h b/src/plugins/python/pythonrunconfiguration.h index 753be4c2968..bb5139e615a 100644 --- a/src/plugins/python/pythonrunconfiguration.h +++ b/src/plugins/python/pythonrunconfiguration.h @@ -42,6 +42,7 @@ class PythonRunConfiguration : public ProjectExplorer::RunConfiguration public: PythonRunConfiguration(ProjectExplorer::Target *target, Core::Id id); + QString interpreter() const; private: void doAdditionalSetup(const ProjectExplorer::RunConfigurationCreationInfo &) final; @@ -49,7 +50,6 @@ private: bool supportsDebugger() const; QString mainScript() const; QString arguments() const; - QString interpreter() const; void updateTargetInformation(); };