// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 &pythonClients() { static QHash clients; return clients; } FilePath getPylsModulePath(CommandLine pylsCommand) { static QMutex mutex; // protect the access to the cache QMutexLocker locker(&mutex); static QMap cache; const FilePath &modulePath = cache.value(pylsCommand.executable()); if (!modulePath.isEmpty()) return modulePath; pylsCommand.addArg("-h"); Process 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); Process 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(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 &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 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->setPackages({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; QPointer 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(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 &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(document)) resetEditorInfoBar(textDocument); }); } } // Python::Internal