aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/python/pythoneditor.cpp
diff options
context:
space:
mode:
authorDavid Schulz <david.schulz@qt.io>2019-07-24 10:19:19 +0200
committerDavid Schulz <david.schulz@qt.io>2019-09-26 07:35:11 +0000
commite232dfe2e67f5513d841716130bb5fe4ac720028 (patch)
treed08f507a4e5804243b5afa560fa80c9fa09317ef /src/plugins/python/pythoneditor.cpp
parentb0f796e4e397cce65dd05ac92c360aef6eaeb8b2 (diff)
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 <christian.stenger@qt.io>
Diffstat (limited to 'src/plugins/python/pythoneditor.cpp')
-rw-r--r--src/plugins/python/pythoneditor.cpp223
1 files changed, 212 insertions, 11 deletions
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 <coreplugin/infobar.h>
+
+#include <languageclient/client.h>
+#include <languageclient/languageclientinterface.h>
+#include <languageclient/languageclientmanager.h>
+
+#include <projectexplorer/session.h>
+#include <projectexplorer/target.h>
+#include <texteditor/textdocument.h>
#include <texteditor/texteditoractionhandler.h>
#include <texteditor/texteditorconstants.h>
-#include <texteditor/textdocument.h>
#include <utils/qtcassert.h>
+#include <utils/synchronousprocess.h>
#include <QCoreApplication>
+#include <QRegularExpression>
-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<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 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<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);
+}
+
+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
+ || 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<TextEditor::TextDocument *>(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