From 986749cdb9ca9078b66297d60bbf21d48e33a6cf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 16 Jul 2025 14:35:46 -0700 Subject: [PATCH] tooling: add automatic QMLLS support for new imports and singletons --- src/core/CMakeLists.txt | 1 + src/core/paths.cpp | 27 ++++++ src/core/paths.hpp | 3 + src/core/rootwrapper.cpp | 27 +++++- src/core/rootwrapper.hpp | 3 + src/core/toolsupport.cpp | 205 +++++++++++++++++++++++++++++++++++++++ src/core/toolsupport.hpp | 20 ++++ 7 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 src/core/toolsupport.cpp create mode 100644 src/core/toolsupport.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eca7270d..7cef987a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -38,6 +38,7 @@ qt_add_library(quickshell-core STATIC iconprovider.cpp scriptmodel.cpp colorquantizer.cpp + toolsupport.cpp ) qt_add_qml_module(quickshell-core diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 1f3c494b..e17c3bcf 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -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")); diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 9646ca41..178bcda1 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -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; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 2968402e..7dc1068f 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -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); +} diff --git a/src/core/rootwrapper.hpp b/src/core/rootwrapper.hpp index 02d7a143..1425d177 100644 --- a/src/core/rootwrapper.hpp +++ b/src/core/rootwrapper.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -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; }; diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp new file mode 100644 index 00000000..febb97fb --- /dev/null +++ b/src/core/toolsupport.cpp @@ -0,0 +1,205 @@ +#include "toolsupport.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 importPaths; + + auto addPaths = [&](const QList& 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 files; + QSet 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 diff --git a/src/core/toolsupport.hpp b/src/core/toolsupport.hpp new file mode 100644 index 00000000..0aee9c5c --- /dev/null +++ b/src/core/toolsupport.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#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