Skip to content

Commit 986749c

Browse files
committed
tooling: add automatic QMLLS support for new imports and singletons
1 parent 4d8055f commit 986749c

File tree

7 files changed

+283
-3
lines changed

7 files changed

+283
-3
lines changed

src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ qt_add_library(quickshell-core STATIC
3838
iconprovider.cpp
3939
scriptmodel.cpp
4040
colorquantizer.cpp
41+
toolsupport.cpp
4142
)
4243

4344
qt_add_qml_module(quickshell-core

src/core/paths.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,33 @@ QDir* QsPaths::instanceRunDir() {
135135
else return &this->mInstanceRunDir;
136136
}
137137

138+
QDir* QsPaths::shellVfsDir() {
139+
if (this->shellVfsState == DirState::Unknown) {
140+
if (auto* baseRunDir = this->baseRunDir()) {
141+
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
142+
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
143+
144+
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
145+
146+
if (!this->mShellVfsDir.mkpath(".")) {
147+
qCCritical(logPaths) << "Could not create runtime vfs directory at"
148+
<< this->mShellVfsDir.path();
149+
this->shellVfsState = DirState::Failed;
150+
} else {
151+
this->shellVfsState = DirState::Ready;
152+
}
153+
} else {
154+
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
155+
"create the base runtime path.";
156+
157+
this->shellVfsState = DirState::Failed;
158+
}
159+
}
160+
161+
if (this->shellVfsState == DirState::Failed) return nullptr;
162+
else return &this->mShellVfsDir;
163+
}
164+
138165
void QsPaths::linkRunDir() {
139166
if (auto* runDir = this->instanceRunDir()) {
140167
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));

src/core/paths.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class QsPaths {
2828

2929
QDir* baseRunDir();
3030
QDir* shellRunDir();
31+
QDir* shellVfsDir();
3132
QDir* instanceRunDir();
3233
void linkRunDir();
3334
void linkPathDir();
@@ -48,9 +49,11 @@ class QsPaths {
4849
QString pathId;
4950
QDir mBaseRunDir;
5051
QDir mShellRunDir;
52+
QDir mShellVfsDir;
5153
QDir mInstanceRunDir;
5254
DirState baseRunState = DirState::Unknown;
5355
DirState shellRunState = DirState::Unknown;
56+
DirState shellVfsState = DirState::Unknown;
5457
DirState instanceRunState = DirState::Unknown;
5558

5659
QDir mShellDataDir;

src/core/rootwrapper.cpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <qdir.h>
66
#include <qfileinfo.h>
7+
#include <qfilesystemwatcher.h>
78
#include <qlogging.h>
89
#include <qobject.h>
910
#include <qqmlcomponent.h>
@@ -18,15 +19,26 @@
1819
#include "instanceinfo.hpp"
1920
#include "qmlglobal.hpp"
2021
#include "scan.hpp"
22+
#include "toolsupport.hpp"
2123

2224
RootWrapper::RootWrapper(QString rootPath, QString shellId)
2325
: QObject(nullptr)
2426
, rootPath(std::move(rootPath))
2527
, shellId(std::move(shellId))
2628
, originalWorkingDirectory(QDir::current().absolutePath()) {
27-
// clang-format off
28-
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
29-
// clang-format on
29+
QObject::connect(
30+
QuickshellSettings::instance(),
31+
&QuickshellSettings::watchFilesChanged,
32+
this,
33+
&RootWrapper::onWatchFilesChanged
34+
);
35+
36+
QObject::connect(
37+
&this->configDirWatcher,
38+
&QFileSystemWatcher::directoryChanged,
39+
this,
40+
&RootWrapper::updateTooling
41+
);
3042

3143
this->reloadGraph(true);
3244

@@ -48,6 +60,9 @@ void RootWrapper::reloadGraph(bool hard) {
4860
auto scanner = QmlScanner(rootPath);
4961
scanner.scanQmlFile(this->rootPath);
5062

63+
qs::core::QmlToolingSupport::updateTooling(rootPath, scanner);
64+
this->configDirWatcher.addPath(rootPath.path());
65+
5166
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
5267
generation->wrapper = this;
5368

@@ -168,3 +183,9 @@ void RootWrapper::onWatchFilesChanged() {
168183
}
169184

170185
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
186+
187+
void RootWrapper::updateTooling() {
188+
if (!this->generation) return;
189+
auto configDir = QFileInfo(this->rootPath).dir();
190+
qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner);
191+
}

src/core/rootwrapper.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <qfilesystemwatcher.h>
34
#include <qobject.h>
45
#include <qqmlengine.h>
56
#include <qtclasshelpermacros.h>
@@ -22,10 +23,12 @@ private slots:
2223
void generationDestroyed();
2324
void onWatchFilesChanged();
2425
void onWatchedFilesChanged();
26+
void updateTooling();
2527

2628
private:
2729
QString rootPath;
2830
QString shellId;
2931
EngineGeneration* generation = nullptr;
3032
QString originalWorkingDirectory;
33+
QFileSystemWatcher configDirWatcher;
3134
};

src/core/toolsupport.cpp

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#include "toolsupport.hpp"
2+
3+
#include <qcontainerfwd.h>
4+
#include <qdebug.h>
5+
#include <qdir.h>
6+
#include <qfileinfo.h>
7+
#include <qlist.h>
8+
#include <qlogging.h>
9+
#include <qloggingcategory.h>
10+
#include <qnamespace.h>
11+
#include <qtenvironmentvariables.h>
12+
13+
#include "logcat.hpp"
14+
#include "paths.hpp"
15+
#include "scan.hpp"
16+
17+
namespace qs::core {
18+
19+
namespace {
20+
QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg);
21+
}
22+
23+
bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) {
24+
auto* vfs = QsPaths::instance()->shellVfsDir();
25+
26+
if (!vfs) {
27+
qCCritical(logTooling) << "Tooling dir could not be created";
28+
return false;
29+
}
30+
31+
if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) {
32+
QDir(vfs->filePath("qs")).removeRecursively();
33+
return false;
34+
}
35+
36+
QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs"));
37+
return true;
38+
}
39+
40+
QString QmlToolingSupport::getQmllsConfig() {
41+
static auto config = []() {
42+
QList<QString> importPaths;
43+
44+
auto addPaths = [&](const QList<QString>& paths) {
45+
for (const auto& path: paths) {
46+
if (!importPaths.contains(path)) importPaths.append(path);
47+
}
48+
};
49+
50+
addPaths(qEnvironmentVariable("QML_IMPORT_PATH").split(u':', Qt::SkipEmptyParts));
51+
addPaths(qEnvironmentVariable("QML2_IMPORT_PATH").split(u':', Qt::SkipEmptyParts));
52+
53+
auto vfsPath = QsPaths::instance()->shellVfsDir()->path();
54+
auto importPathsStr = importPaths.join(u':');
55+
56+
QString qmllsConfig;
57+
auto print = QDebug(&qmllsConfig).nospace();
58+
print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath
59+
<< "\nimportPaths=" << importPathsStr << '\n';
60+
61+
return qmllsConfig;
62+
}();
63+
64+
return config;
65+
}
66+
67+
bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) {
68+
auto shellConfigPath = configRoot.filePath(".qmlls.ini");
69+
auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini");
70+
71+
auto shellFileInfo = QFileInfo(shellConfigPath);
72+
if (!create && !shellFileInfo.exists()) {
73+
if (QmlToolingSupport::toolingEnabled) {
74+
qInfo() << "QML tooling support disabled";
75+
QmlToolingSupport::toolingEnabled = false;
76+
}
77+
78+
QFile::remove(vfsConfigPath);
79+
return false;
80+
}
81+
82+
auto vfsFile = QFile(vfsConfigPath);
83+
84+
if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) {
85+
qCCritical(logTooling) << "Failed to create qmlls config in vfs";
86+
return false;
87+
}
88+
89+
auto config = QmlToolingSupport::getQmllsConfig();
90+
91+
if (vfsFile.readAll() != config) {
92+
if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) {
93+
qCCritical(logTooling) << "Failed to write qmlls config in vfs";
94+
return false;
95+
}
96+
97+
qCDebug(logTooling) << "Wrote qmlls config in vfs";
98+
}
99+
100+
if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) {
101+
QFile::remove(shellConfigPath);
102+
103+
if (!QFile::link(vfsConfigPath, shellConfigPath)) {
104+
qCCritical(logTooling) << "Failed to create qmlls config symlink";
105+
return false;
106+
}
107+
108+
qCDebug(logTooling) << "Created qmlls config symlink";
109+
}
110+
111+
if (!QmlToolingSupport::toolingEnabled) {
112+
qInfo() << "QML tooling support enabled";
113+
QmlToolingSupport::toolingEnabled = true;
114+
}
115+
116+
return true;
117+
}
118+
119+
void QmlToolingSupport::updateToolingFs(
120+
QmlScanner& scanner,
121+
const QDir& scanDir,
122+
const QDir& linkDir
123+
) {
124+
QList<QString> files;
125+
QSet<QString> subdirs;
126+
127+
auto scanPath = scanDir.path();
128+
129+
linkDir.mkpath(".");
130+
131+
for (auto& path: scanner.scannedFiles) {
132+
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
133+
auto name = path.sliced(scanPath.length() + 1);
134+
135+
if (name.contains('/')) {
136+
auto dirname = name.first(name.indexOf('/'));
137+
subdirs.insert(dirname);
138+
continue;
139+
}
140+
141+
auto fileInfo = QFileInfo(path);
142+
if (!fileInfo.isFile()) continue;
143+
144+
auto spath = linkDir.filePath(name);
145+
auto sFileInfo = QFileInfo(spath);
146+
147+
if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) {
148+
QFile::remove(spath);
149+
150+
if (QFile::link(path, spath)) {
151+
qCDebug(logTooling) << "Created symlink to" << path << "at" << spath;
152+
files.append(spath);
153+
} else {
154+
qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath;
155+
}
156+
} else {
157+
files.append(spath);
158+
}
159+
}
160+
161+
for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) {
162+
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
163+
auto name = path.sliced(scanPath.length() + 1);
164+
165+
if (name.contains('/')) {
166+
auto dirname = name.first(name.indexOf('/'));
167+
subdirs.insert(dirname);
168+
continue;
169+
}
170+
171+
auto spath = linkDir.filePath(name);
172+
auto file = QFile(spath);
173+
if (!file.open(QFile::ReadWrite | QFile::Text)) {
174+
qCCritical(logTooling) << "Failed to open injected file" << spath;
175+
continue;
176+
}
177+
178+
if (file.readAll() == text) {
179+
files.append(spath);
180+
continue;
181+
}
182+
183+
if (file.resize(0) && file.write(text.toUtf8())) {
184+
files.append(spath);
185+
qCDebug(logTooling) << "Wrote injected file" << spath;
186+
} else {
187+
qCCritical(logTooling) << "Failed to write injected file" << spath;
188+
}
189+
}
190+
191+
for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks
192+
auto path = linkDir.filePath(name);
193+
194+
if (!files.contains(path)) {
195+
if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path;
196+
else qCWarning(logTooling) << "Failed to remove old file at" << path;
197+
}
198+
}
199+
200+
for (const auto& subdir: subdirs) {
201+
QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir));
202+
}
203+
}
204+
205+
} // namespace qs::core

src/core/toolsupport.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#pragma once
2+
3+
#include <qdir.h>
4+
5+
#include "scan.hpp"
6+
7+
namespace qs::core {
8+
9+
class QmlToolingSupport {
10+
public:
11+
static bool updateTooling(const QDir& configRoot, QmlScanner& scanner);
12+
13+
private:
14+
static QString getQmllsConfig();
15+
static bool updateQmllsConfig(const QDir& configRoot, bool create);
16+
static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir);
17+
static inline bool toolingEnabled = false;
18+
};
19+
20+
} // namespace qs::core

0 commit comments

Comments
 (0)