forked from qt-creator/qt-creator
Change-Id: I26254d5c78c3b6ea06ed9baec8e52b988a6e9608 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: David Schulz <david.schulz@qt.io> Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
404 lines
14 KiB
C++
404 lines
14 KiB
C++
// Copyright (C) 2021 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
#include "pythonlanguageclient.h"
|
|
|
|
#include "pipsupport.h"
|
|
#include "pysideuicextracompiler.h"
|
|
#include "pythonconstants.h"
|
|
#include "pythonplugin.h"
|
|
#include "pythonproject.h"
|
|
#include "pythonrunconfiguration.h"
|
|
#include "pythonsettings.h"
|
|
#include "pythontr.h"
|
|
#include "pythonutils.h"
|
|
|
|
#include <coreplugin/editormanager/editormanager.h>
|
|
#include <coreplugin/icore.h>
|
|
#include <coreplugin/progressmanager/progressmanager.h>
|
|
|
|
#include <languageclient/languageclientinterface.h>
|
|
#include <languageclient/languageclientmanager.h>
|
|
#include <languageserverprotocol/textsynchronization.h>
|
|
#include <languageserverprotocol/workspace.h>
|
|
|
|
#include <projectexplorer/extracompiler.h>
|
|
#include <projectexplorer/projectmanager.h>
|
|
#include <projectexplorer/target.h>
|
|
|
|
#include <texteditor/textdocument.h>
|
|
#include <texteditor/texteditor.h>
|
|
|
|
#include <utils/asynctask.h>
|
|
#include <utils/infobar.h>
|
|
#include <utils/qtcprocess.h>
|
|
#include <utils/variablechooser.h>
|
|
|
|
#include <QCheckBox>
|
|
#include <QComboBox>
|
|
#include <QFutureWatcher>
|
|
#include <QGroupBox>
|
|
#include <QJsonDocument>
|
|
#include <QPushButton>
|
|
#include <QRegularExpression>
|
|
#include <QTimer>
|
|
|
|
using namespace LanguageClient;
|
|
using namespace LanguageServerProtocol;
|
|
using namespace ProjectExplorer;
|
|
using namespace Utils;
|
|
|
|
namespace Python::Internal {
|
|
|
|
static constexpr char installPylsInfoBarId[] = "Python::InstallPyls";
|
|
|
|
class PythonLanguageServerState
|
|
{
|
|
public:
|
|
enum {
|
|
CanNotBeInstalled,
|
|
CanBeInstalled,
|
|
AlreadyInstalled
|
|
} state;
|
|
FilePath pylsModulePath;
|
|
};
|
|
|
|
static QHash<FilePath, PyLSClient*> &pythonClients()
|
|
{
|
|
static QHash<FilePath, PyLSClient*> clients;
|
|
return clients;
|
|
}
|
|
|
|
FilePath getPylsModulePath(CommandLine pylsCommand)
|
|
{
|
|
static QMutex mutex; // protect the access to the cache
|
|
QMutexLocker locker(&mutex);
|
|
static QMap<FilePath, FilePath> cache;
|
|
const FilePath &modulePath = cache.value(pylsCommand.executable());
|
|
if (!modulePath.isEmpty())
|
|
return modulePath;
|
|
|
|
pylsCommand.addArg("-h");
|
|
|
|
QtcProcess pythonProcess;
|
|
Environment env = pythonProcess.environment();
|
|
env.set("PYTHONVERBOSE", "x");
|
|
pythonProcess.setEnvironment(env);
|
|
pythonProcess.setCommand(pylsCommand);
|
|
pythonProcess.runBlocking();
|
|
|
|
static const QString pylsInitPattern = "(.*)"
|
|
+ QRegularExpression::escape(
|
|
QDir::toNativeSeparators("/pylsp/__init__.py"))
|
|
+ '$';
|
|
static const QRegularExpression regexCached(" matches " + pylsInitPattern,
|
|
QRegularExpression::MultilineOption);
|
|
static const QRegularExpression regexNotCached(" code object from " + pylsInitPattern,
|
|
QRegularExpression::MultilineOption);
|
|
|
|
const QString output = pythonProcess.allOutput();
|
|
for (const auto ®ex : {regexCached, regexNotCached}) {
|
|
const QRegularExpressionMatch result = regex.match(output);
|
|
if (result.hasMatch()) {
|
|
const FilePath &modulePath = FilePath::fromUserInput(result.captured(1));
|
|
cache[pylsCommand.executable()] = modulePath;
|
|
return modulePath;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static PythonLanguageServerState checkPythonLanguageServer(const FilePath &python)
|
|
{
|
|
using namespace LanguageClient;
|
|
const CommandLine pythonLShelpCommand(python, {"-m", "pylsp", "-h"});
|
|
const FilePath &modulePath = getPylsModulePath(pythonLShelpCommand);
|
|
|
|
QtcProcess pythonProcess;
|
|
pythonProcess.setCommand(pythonLShelpCommand);
|
|
pythonProcess.runBlocking();
|
|
if (pythonProcess.allOutput().contains("Python Language Server"))
|
|
return {PythonLanguageServerState::AlreadyInstalled, modulePath};
|
|
|
|
pythonProcess.setCommand({python, {"-m", "pip", "-V"}});
|
|
pythonProcess.runBlocking();
|
|
if (pythonProcess.allOutput().startsWith("pip "))
|
|
return {PythonLanguageServerState::CanBeInstalled, FilePath()};
|
|
else
|
|
return {PythonLanguageServerState::CanNotBeInstalled, FilePath()};
|
|
}
|
|
|
|
|
|
class PyLSInterface : public StdIOClientInterface
|
|
{
|
|
public:
|
|
PyLSInterface()
|
|
: m_extraPythonPath("QtCreator-pyls-XXXXXX")
|
|
{ }
|
|
|
|
TemporaryDirectory m_extraPythonPath;
|
|
protected:
|
|
void startImpl() override
|
|
{
|
|
if (!m_cmd.executable().needsDevice()) {
|
|
// todo check where to put this tempdir in remote setups
|
|
Environment env = Environment::systemEnvironment();
|
|
env.appendOrSet("PYTHONPATH",
|
|
m_extraPythonPath.path().toString(),
|
|
OsSpecificAspects::pathListSeparator(env.osType()));
|
|
setEnvironment(env);
|
|
}
|
|
StdIOClientInterface::startImpl();
|
|
}
|
|
};
|
|
|
|
PyLSClient *clientForPython(const FilePath &python)
|
|
{
|
|
if (auto client = pythonClients()[python])
|
|
return client;
|
|
auto interface = new PyLSInterface;
|
|
interface->setCommandLine(CommandLine(python, {"-m", "pylsp"}));
|
|
auto client = new PyLSClient(interface);
|
|
client->setName(Tr::tr("Python Language Server (%1)").arg(python.toUserOutput()));
|
|
client->setActivateDocumentAutomatically(true);
|
|
client->updateConfiguration();
|
|
LanguageFilter filter;
|
|
filter.mimeTypes = QStringList() << Constants::C_PY_MIMETYPE << Constants::C_PY3_MIMETYPE;
|
|
client->setSupportedLanguage(filter);
|
|
client->start();
|
|
pythonClients()[python] = client;
|
|
return client;
|
|
}
|
|
|
|
PyLSClient::PyLSClient(PyLSInterface *interface)
|
|
: Client(interface)
|
|
, m_extraCompilerOutputDir(interface->m_extraPythonPath.path())
|
|
{
|
|
connect(this, &Client::initialized, this, &PyLSClient::updateConfiguration);
|
|
connect(PythonSettings::instance(), &PythonSettings::pylsConfigurationChanged,
|
|
this, &PyLSClient::updateConfiguration);
|
|
connect(PythonSettings::instance(), &PythonSettings::pylsEnabledChanged,
|
|
this, [this](const bool enabled){
|
|
if (!enabled)
|
|
LanguageClientManager::shutdownClient(this);
|
|
});
|
|
|
|
}
|
|
|
|
PyLSClient::~PyLSClient()
|
|
{
|
|
pythonClients().remove(pythonClients().key(this));
|
|
}
|
|
|
|
void PyLSClient::updateConfiguration()
|
|
{
|
|
const auto doc = QJsonDocument::fromJson(PythonSettings::pylsConfiguration().toUtf8());
|
|
if (doc.isArray())
|
|
Client::updateConfiguration(doc.array());
|
|
else if (doc.isObject())
|
|
Client::updateConfiguration(doc.object());
|
|
}
|
|
|
|
void PyLSClient::openDocument(TextEditor::TextDocument *document)
|
|
{
|
|
using namespace LanguageServerProtocol;
|
|
if (reachable()) {
|
|
const FilePath documentPath = document->filePath();
|
|
if (PythonProject *project = pythonProjectForFile(documentPath)) {
|
|
if (Target *target = project->activeTarget()) {
|
|
if (auto rc = qobject_cast<PythonRunConfiguration *>(target->activeRunConfiguration()))
|
|
updateExtraCompilers(project, rc->extraCompilers());
|
|
}
|
|
} else if (isSupportedDocument(document)) {
|
|
const FilePath workspacePath = documentPath.parentDir();
|
|
if (!m_extraWorkspaceDirs.contains(workspacePath)) {
|
|
WorkspaceFoldersChangeEvent event;
|
|
event.setAdded({WorkSpaceFolder(hostPathToServerUri(workspacePath),
|
|
workspacePath.fileName())});
|
|
DidChangeWorkspaceFoldersParams params;
|
|
params.setEvent(event);
|
|
DidChangeWorkspaceFoldersNotification change(params);
|
|
sendMessage(change);
|
|
m_extraWorkspaceDirs.append(workspacePath);
|
|
}
|
|
}
|
|
}
|
|
Client::openDocument(document);
|
|
}
|
|
|
|
void PyLSClient::projectClosed(ProjectExplorer::Project *project)
|
|
{
|
|
for (ProjectExplorer::ExtraCompiler *compiler : m_extraCompilers.value(project))
|
|
closeExtraCompiler(compiler);
|
|
Client::projectClosed(project);
|
|
}
|
|
|
|
void PyLSClient::updateExtraCompilers(ProjectExplorer::Project *project,
|
|
const QList<PySideUicExtraCompiler *> &extraCompilers)
|
|
{
|
|
auto oldCompilers = m_extraCompilers.take(project);
|
|
for (PySideUicExtraCompiler *extraCompiler : extraCompilers) {
|
|
QTC_ASSERT(extraCompiler->targets().size() == 1 , continue);
|
|
int index = oldCompilers.indexOf(extraCompiler);
|
|
if (index < 0) {
|
|
m_extraCompilers[project] << extraCompiler;
|
|
connect(extraCompiler,
|
|
&ExtraCompiler::contentsChanged,
|
|
this,
|
|
[this, extraCompiler](const FilePath &file) {
|
|
updateExtraCompilerContents(extraCompiler, file);
|
|
});
|
|
if (extraCompiler->isDirty())
|
|
extraCompiler->compileFile();
|
|
} else {
|
|
m_extraCompilers[project] << oldCompilers.takeAt(index);
|
|
}
|
|
}
|
|
for (ProjectExplorer::ExtraCompiler *compiler : oldCompilers)
|
|
closeExtraCompiler(compiler);
|
|
}
|
|
|
|
void PyLSClient::updateExtraCompilerContents(ExtraCompiler *compiler, const FilePath &file)
|
|
{
|
|
const FilePath target = m_extraCompilerOutputDir.pathAppended(file.fileName());
|
|
|
|
target.writeFileContents(compiler->content(file));
|
|
}
|
|
|
|
void PyLSClient::closeExtraCompiler(ProjectExplorer::ExtraCompiler *compiler)
|
|
{
|
|
const FilePath file = compiler->targets().constFirst();
|
|
m_extraCompilerOutputDir.pathAppended(file.fileName()).removeFile();
|
|
compiler->disconnect(this);
|
|
}
|
|
|
|
PyLSClient *PyLSClient::clientForPython(const FilePath &python)
|
|
{
|
|
return pythonClients()[python];
|
|
}
|
|
|
|
PyLSConfigureAssistant *PyLSConfigureAssistant::instance()
|
|
{
|
|
static auto *instance = new PyLSConfigureAssistant(PythonPlugin::instance());
|
|
return instance;
|
|
}
|
|
|
|
void PyLSConfigureAssistant::installPythonLanguageServer(const FilePath &python,
|
|
QPointer<TextEditor::TextDocument> document)
|
|
{
|
|
document->infoBar()->removeInfo(installPylsInfoBarId);
|
|
|
|
// Hide all install info bar entries for this python, but keep them in the list
|
|
// so the language server will be setup properly after the installation is done.
|
|
for (TextEditor::TextDocument *additionalDocument : m_infoBarEntries[python])
|
|
additionalDocument->infoBar()->removeInfo(installPylsInfoBarId);
|
|
|
|
auto install = new PipInstallTask(python);
|
|
|
|
connect(install, &PipInstallTask::finished, this, [=](const bool success) {
|
|
if (success) {
|
|
if (document) {
|
|
if (PyLSClient *client = clientForPython(python))
|
|
LanguageClientManager::openDocumentWithClient(document, client);
|
|
}
|
|
}
|
|
install->deleteLater();
|
|
});
|
|
|
|
install->setPackage(PipPackage{"python-lsp-server[all]", "Python Language Server"});
|
|
install->run();
|
|
}
|
|
|
|
void PyLSConfigureAssistant::openDocumentWithPython(const FilePath &python,
|
|
TextEditor::TextDocument *document)
|
|
{
|
|
instance()->resetEditorInfoBar(document);
|
|
if (!PythonSettings::pylsEnabled())
|
|
return;
|
|
|
|
if (auto client = pythonClients().value(python)) {
|
|
LanguageClientManager::openDocumentWithClient(document, client);
|
|
return;
|
|
}
|
|
|
|
using CheckPylsWatcher = QFutureWatcher<PythonLanguageServerState>;
|
|
QPointer<CheckPylsWatcher> watcher = new CheckPylsWatcher();
|
|
|
|
// cancel and delete watcher after a 10 second timeout
|
|
QTimer::singleShot(10000, instance(), [watcher]() {
|
|
if (watcher) {
|
|
watcher->cancel();
|
|
watcher->deleteLater();
|
|
}
|
|
});
|
|
|
|
connect(watcher,
|
|
&CheckPylsWatcher::resultReadyAt,
|
|
instance(),
|
|
[=, document = QPointer<TextEditor::TextDocument>(document)]() {
|
|
if (!document || !watcher)
|
|
return;
|
|
instance()->handlePyLSState(python, watcher->result(), document);
|
|
watcher->deleteLater();
|
|
});
|
|
watcher->setFuture(Utils::asyncRun(&checkPythonLanguageServer, python));
|
|
}
|
|
|
|
void PyLSConfigureAssistant::handlePyLSState(const FilePath &python,
|
|
const PythonLanguageServerState &state,
|
|
TextEditor::TextDocument *document)
|
|
{
|
|
if (state.state == PythonLanguageServerState::CanNotBeInstalled)
|
|
return;
|
|
|
|
Utils::InfoBar *infoBar = document->infoBar();
|
|
if (state.state == PythonLanguageServerState::CanBeInstalled
|
|
&& infoBar->canInfoBeAdded(installPylsInfoBarId)) {
|
|
auto message = Tr::tr("Install Python language server (PyLS) for %1 (%2). "
|
|
"The language server provides Python specific completion and annotation.")
|
|
.arg(pythonName(python), python.toUserOutput());
|
|
Utils::InfoBarEntry info(installPylsInfoBarId,
|
|
message,
|
|
Utils::InfoBarEntry::GlobalSuppression::Enabled);
|
|
info.addCustomButton(Tr::tr("Install"),
|
|
[=]() { installPythonLanguageServer(python, document); });
|
|
infoBar->addInfo(info);
|
|
m_infoBarEntries[python] << document;
|
|
} else if (state.state == PythonLanguageServerState::AlreadyInstalled) {
|
|
if (auto client = clientForPython(python))
|
|
LanguageClientManager::openDocumentWithClient(document, client);
|
|
}
|
|
}
|
|
|
|
void PyLSConfigureAssistant::updateEditorInfoBars(const FilePath &python, Client *client)
|
|
{
|
|
for (TextEditor::TextDocument *document : instance()->m_infoBarEntries.take(python)) {
|
|
instance()->resetEditorInfoBar(document);
|
|
if (client)
|
|
LanguageClientManager::openDocumentWithClient(document, client);
|
|
}
|
|
}
|
|
|
|
void PyLSConfigureAssistant::resetEditorInfoBar(TextEditor::TextDocument *document)
|
|
{
|
|
for (QList<TextEditor::TextDocument *> &documents : m_infoBarEntries)
|
|
documents.removeAll(document);
|
|
document->infoBar()->removeInfo(installPylsInfoBarId);
|
|
}
|
|
|
|
PyLSConfigureAssistant::PyLSConfigureAssistant(QObject *parent)
|
|
: QObject(parent)
|
|
{
|
|
Core::EditorManager::instance();
|
|
|
|
connect(Core::EditorManager::instance(),
|
|
&Core::EditorManager::documentClosed,
|
|
this,
|
|
[this](Core::IDocument *document) {
|
|
if (auto textDocument = qobject_cast<TextEditor::TextDocument *>(document))
|
|
resetEditorInfoBar(textDocument);
|
|
});
|
|
}
|
|
|
|
} // Python::Internal
|