tooling: add automatic QMLLS support for new imports and singletons

This commit is contained in:
outfoxxed 2025-07-16 14:35:46 -07:00
parent 4d8055f1cd
commit 986749cdb9
Signed by untrusted user: outfoxxed
GPG key ID: 4C88A185FB89301E
7 changed files with 283 additions and 3 deletions

View file

@ -38,6 +38,7 @@ qt_add_library(quickshell-core STATIC
iconprovider.cpp
scriptmodel.cpp
colorquantizer.cpp
toolsupport.cpp
)
qt_add_qml_module(quickshell-core

View file

@ -135,6 +135,33 @@ QDir* QsPaths::instanceRunDir() {
else return &this->mInstanceRunDir;
}
QDir* QsPaths::shellVfsDir() {
if (this->shellVfsState == DirState::Unknown) {
if (auto* baseRunDir = this->baseRunDir()) {
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
if (!this->mShellVfsDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create runtime vfs directory at"
<< this->mShellVfsDir.path();
this->shellVfsState = DirState::Failed;
} else {
this->shellVfsState = DirState::Ready;
}
} else {
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
"create the base runtime path.";
this->shellVfsState = DirState::Failed;
}
}
if (this->shellVfsState == DirState::Failed) return nullptr;
else return &this->mShellVfsDir;
}
void QsPaths::linkRunDir() {
if (auto* runDir = this->instanceRunDir()) {
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));

View file

@ -28,6 +28,7 @@ public:
QDir* baseRunDir();
QDir* shellRunDir();
QDir* shellVfsDir();
QDir* instanceRunDir();
void linkRunDir();
void linkPathDir();
@ -48,9 +49,11 @@ private:
QString pathId;
QDir mBaseRunDir;
QDir mShellRunDir;
QDir mShellVfsDir;
QDir mInstanceRunDir;
DirState baseRunState = DirState::Unknown;
DirState shellRunState = DirState::Unknown;
DirState shellVfsState = DirState::Unknown;
DirState instanceRunState = DirState::Unknown;
QDir mShellDataDir;

View file

@ -4,6 +4,7 @@
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
@ -18,15 +19,26 @@
#include "instanceinfo.hpp"
#include "qmlglobal.hpp"
#include "scan.hpp"
#include "toolsupport.hpp"
RootWrapper::RootWrapper(QString rootPath, QString shellId)
: QObject(nullptr)
, rootPath(std::move(rootPath))
, shellId(std::move(shellId))
, originalWorkingDirectory(QDir::current().absolutePath()) {
// clang-format off
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
// clang-format on
QObject::connect(
QuickshellSettings::instance(),
&QuickshellSettings::watchFilesChanged,
this,
&RootWrapper::onWatchFilesChanged
);
QObject::connect(
&this->configDirWatcher,
&QFileSystemWatcher::directoryChanged,
this,
&RootWrapper::updateTooling
);
this->reloadGraph(true);
@ -48,6 +60,9 @@ void RootWrapper::reloadGraph(bool hard) {
auto scanner = QmlScanner(rootPath);
scanner.scanQmlFile(this->rootPath);
qs::core::QmlToolingSupport::updateTooling(rootPath, scanner);
this->configDirWatcher.addPath(rootPath.path());
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
generation->wrapper = this;
@ -168,3 +183,9 @@ void RootWrapper::onWatchFilesChanged() {
}
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
void RootWrapper::updateTooling() {
if (!this->generation) return;
auto configDir = QFileInfo(this->rootPath).dir();
qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner);
}

View file

@ -1,5 +1,6 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qtclasshelpermacros.h>
@ -22,10 +23,12 @@ private slots:
void generationDestroyed();
void onWatchFilesChanged();
void onWatchedFilesChanged();
void updateTooling();
private:
QString rootPath;
QString shellId;
EngineGeneration* generation = nullptr;
QString originalWorkingDirectory;
QFileSystemWatcher configDirWatcher;
};

205
src/core/toolsupport.cpp Normal file
View file

@ -0,0 +1,205 @@
#include "toolsupport.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qtenvironmentvariables.h>
#include "logcat.hpp"
#include "paths.hpp"
#include "scan.hpp"
namespace qs::core {
namespace {
QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg);
}
bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) {
auto* vfs = QsPaths::instance()->shellVfsDir();
if (!vfs) {
qCCritical(logTooling) << "Tooling dir could not be created";
return false;
}
if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) {
QDir(vfs->filePath("qs")).removeRecursively();
return false;
}
QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs"));
return true;
}
QString QmlToolingSupport::getQmllsConfig() {
static auto config = []() {
QList<QString> importPaths;
auto addPaths = [&](const QList<QString>& paths) {
for (const auto& path: paths) {
if (!importPaths.contains(path)) importPaths.append(path);
}
};
addPaths(qEnvironmentVariable("QML_IMPORT_PATH").split(u':', Qt::SkipEmptyParts));
addPaths(qEnvironmentVariable("QML2_IMPORT_PATH").split(u':', Qt::SkipEmptyParts));
auto vfsPath = QsPaths::instance()->shellVfsDir()->path();
auto importPathsStr = importPaths.join(u':');
QString qmllsConfig;
auto print = QDebug(&qmllsConfig).nospace();
print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath
<< "\nimportPaths=" << importPathsStr << '\n';
return qmllsConfig;
}();
return config;
}
bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) {
auto shellConfigPath = configRoot.filePath(".qmlls.ini");
auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini");
auto shellFileInfo = QFileInfo(shellConfigPath);
if (!create && !shellFileInfo.exists()) {
if (QmlToolingSupport::toolingEnabled) {
qInfo() << "QML tooling support disabled";
QmlToolingSupport::toolingEnabled = false;
}
QFile::remove(vfsConfigPath);
return false;
}
auto vfsFile = QFile(vfsConfigPath);
if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) {
qCCritical(logTooling) << "Failed to create qmlls config in vfs";
return false;
}
auto config = QmlToolingSupport::getQmllsConfig();
if (vfsFile.readAll() != config) {
if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) {
qCCritical(logTooling) << "Failed to write qmlls config in vfs";
return false;
}
qCDebug(logTooling) << "Wrote qmlls config in vfs";
}
if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) {
QFile::remove(shellConfigPath);
if (!QFile::link(vfsConfigPath, shellConfigPath)) {
qCCritical(logTooling) << "Failed to create qmlls config symlink";
return false;
}
qCDebug(logTooling) << "Created qmlls config symlink";
}
if (!QmlToolingSupport::toolingEnabled) {
qInfo() << "QML tooling support enabled";
QmlToolingSupport::toolingEnabled = true;
}
return true;
}
void QmlToolingSupport::updateToolingFs(
QmlScanner& scanner,
const QDir& scanDir,
const QDir& linkDir
) {
QList<QString> files;
QSet<QString> subdirs;
auto scanPath = scanDir.path();
linkDir.mkpath(".");
for (auto& path: scanner.scannedFiles) {
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
auto name = path.sliced(scanPath.length() + 1);
if (name.contains('/')) {
auto dirname = name.first(name.indexOf('/'));
subdirs.insert(dirname);
continue;
}
auto fileInfo = QFileInfo(path);
if (!fileInfo.isFile()) continue;
auto spath = linkDir.filePath(name);
auto sFileInfo = QFileInfo(spath);
if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) {
QFile::remove(spath);
if (QFile::link(path, spath)) {
qCDebug(logTooling) << "Created symlink to" << path << "at" << spath;
files.append(spath);
} else {
qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath;
}
} else {
files.append(spath);
}
}
for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) {
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
auto name = path.sliced(scanPath.length() + 1);
if (name.contains('/')) {
auto dirname = name.first(name.indexOf('/'));
subdirs.insert(dirname);
continue;
}
auto spath = linkDir.filePath(name);
auto file = QFile(spath);
if (!file.open(QFile::ReadWrite | QFile::Text)) {
qCCritical(logTooling) << "Failed to open injected file" << spath;
continue;
}
if (file.readAll() == text) {
files.append(spath);
continue;
}
if (file.resize(0) && file.write(text.toUtf8())) {
files.append(spath);
qCDebug(logTooling) << "Wrote injected file" << spath;
} else {
qCCritical(logTooling) << "Failed to write injected file" << spath;
}
}
for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks
auto path = linkDir.filePath(name);
if (!files.contains(path)) {
if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path;
else qCWarning(logTooling) << "Failed to remove old file at" << path;
}
}
for (const auto& subdir: subdirs) {
QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir));
}
}
} // namespace qs::core

20
src/core/toolsupport.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <qdir.h>
#include "scan.hpp"
namespace qs::core {
class QmlToolingSupport {
public:
static bool updateTooling(const QDir& configRoot, QmlScanner& scanner);
private:
static QString getQmllsConfig();
static bool updateQmllsConfig(const QDir& configRoot, bool create);
static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir);
static inline bool toolingEnabled = false;
};
} // namespace qs::core