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>
This commit is contained in:
David Schulz
2019-07-24 10:19:19 +02:00
parent b0f796e4e3
commit e232dfe2e6
5 changed files with 218 additions and 15 deletions

View File

@@ -1,5 +1,5 @@
add_qtc_plugin(Python add_qtc_plugin(Python
PLUGIN_DEPENDS Core ProjectExplorer TextEditor PLUGIN_DEPENDS Core LanguageClient ProjectExplorer TextEditor
SOURCES SOURCES
python.qrc python.qrc
pythoneditor.cpp pythoneditor.h pythoneditor.cpp pythoneditor.h

View File

@@ -9,6 +9,7 @@ QtcPlugin {
Depends { name: "Core" } Depends { name: "Core" }
Depends { name: "TextEditor" } Depends { name: "TextEditor" }
Depends { name: "ProjectExplorer" } Depends { name: "ProjectExplorer" }
Depends { name: "LanguageClient" }
Group { Group {
name: "General" name: "General"

View File

@@ -4,5 +4,6 @@ QTC_LIB_DEPENDS += \
utils utils
QTC_PLUGIN_DEPENDS += \ QTC_PLUGIN_DEPENDS += \
coreplugin \ coreplugin \
texteditor \ languageclient \
projectexplorer projectexplorer \
texteditor

View File

@@ -25,40 +25,241 @@
#include "pythoneditor.h" #include "pythoneditor.h"
#include "pythonconstants.h" #include "pythonconstants.h"
#include "pythonplugin.h"
#include "pythonindenter.h"
#include "pythonhighlighter.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/texteditoractionhandler.h>
#include <texteditor/texteditorconstants.h> #include <texteditor/texteditorconstants.h>
#include <texteditor/textdocument.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <utils/synchronousprocess.h>
#include <QCoreApplication> #include <QCoreApplication>
#include <QRegularExpression>
using namespace TextEditor; using namespace ProjectExplorer;
using namespace Utils;
namespace Python { namespace Python {
namespace Internal { 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() PythonEditorFactory::PythonEditorFactory()
{ {
setId(Constants::C_PYTHONEDITOR_ID); 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); addMimeType(Constants::C_PY_MIMETYPE);
setEditorActionHandlers(TextEditorActionHandler::Format setEditorActionHandlers(TextEditor::TextEditorActionHandler::Format
| TextEditorActionHandler::UnCommentSelection | TextEditor::TextEditorActionHandler::UnCommentSelection
| TextEditorActionHandler::UnCollapseAll | TextEditor::TextEditorActionHandler::UnCollapseAll
| TextEditorActionHandler::FollowSymbolUnderCursor); | 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); }); setIndenterCreator([](QTextDocument *doc) { return new PythonIndenter(doc); });
setSyntaxHighlighterCreator([] { return new PythonHighlighter; }); setSyntaxHighlighterCreator([] { return new PythonHighlighter; });
setCommentDefinition(Utils::CommentDefinition::HashStyle); setCommentDefinition(CommentDefinition::HashStyle);
setParenthesesMatchingEnabled(true); setParenthesesMatchingEnabled(true);
setCodeFoldingSupported(true); setCodeFoldingSupported(true);
connect(Core::EditorManager::instance(), &Core::EditorManager::documentOpened,
this, documentOpened);
} }
} // namespace Internal } // namespace Internal

View File

@@ -42,6 +42,7 @@ class PythonRunConfiguration : public ProjectExplorer::RunConfiguration
public: public:
PythonRunConfiguration(ProjectExplorer::Target *target, Core::Id id); PythonRunConfiguration(ProjectExplorer::Target *target, Core::Id id);
QString interpreter() const;
private: private:
void doAdditionalSetup(const ProjectExplorer::RunConfigurationCreationInfo &) final; void doAdditionalSetup(const ProjectExplorer::RunConfigurationCreationInfo &) final;
@@ -49,7 +50,6 @@ private:
bool supportsDebugger() const; bool supportsDebugger() const;
QString mainScript() const; QString mainScript() const;
QString arguments() const; QString arguments() const;
QString interpreter() const;
void updateTargetInformation(); void updateTargetInformation();
}; };