1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qmllsmain_p.h"
5
6#include <private/qhttpmessagestreamparser_p.h>
7#include <private/qqmlglobal_p.h>
8#include <private/qqmljscompiler_p.h>
9#include <private/qqmljsimporter_p.h>
10#include <private/qqmljslogger_p.h>
11#include <private/qqmljsresourcefilemapper_p.h>
12#include <private/qqmljsscope_p.h>
13#include <private/qqmllanguageserver_p.h>
14#include <private/qqmltoolingsettings_p.h>
15#include <private/qqmltoolingutils_p.h>
16
17#include <QtCore/qdebug.h>
18#include <QtCore/qfile.h>
19#include <QtCore/qdir.h>
20#include <QtCore/qfileinfo.h>
21#include <QtCore/qcoreapplication.h>
22#include <QtCore/qdiriterator.h>
23#include <QtCore/qjsonobject.h>
24#include <QtCore/qjsonarray.h>
25#include <QtCore/qjsondocument.h>
26#include <QtCore/qmutex.h>
27#include <QtCore/QMutexLocker>
28#include <QtCore/qscopedpointer.h>
29#include <QtCore/qrunnable.h>
30#include <QtCore/qthreadpool.h>
31#include <QtCore/qtimer.h>
32
33#if QT_CONFIG(commandlineparser)
34# include <QtCore/qcommandlineparser.h>
35#endif
36
37#ifndef QT_BOOTSTRAPPED
38# include <QtCore/qlibraryinfo.h>
39#endif
40
41#include <iostream>
42#ifdef Q_OS_WIN32
43# include <fcntl.h>
44# include <io.h>
45#endif
46
47QT_BEGIN_NAMESPACE
48
49namespace QmlLsp {
50
51QFile *logFile = nullptr;
52QBasicMutex *logFileLock = nullptr;
53
54class StdinReader : public QObject
55{
56 Q_OBJECT
57public:
58 StdinReader()
59 : m_streamReader(
60 [](const QByteArray &, const QByteArray &) { /* just a header, do nothing */ },
61 [this](const QByteArray &) {
62 // stop reading until we are sure that the server is not shutting down
63 m_isReading = false;
64
65 // message body
66 m_shouldSendData = true;
67 },
68 [this](QtMsgType, QString) {
69 // there was an error
70 m_shouldSendData = true;
71 },
72 QHttpMessageStreamParser::UNBUFFERED)
73 {
74 }
75
76 void sendData()
77 {
78 const bool isEndOfMessage = !m_isReading && !m_hasEof;
79 const qsizetype toSend = m_bytesInBuf;
80 m_bytesInBuf = 0;
81 const QByteArray dataToSend(m_buffer, toSend);
82 emit receivedData(data: dataToSend, canRequestMoreData: isEndOfMessage);
83 }
84
85private:
86 const static constexpr qsizetype s_bufSize = 1024;
87 qsizetype m_bytesInBuf = 0;
88 char m_buffer[2 * s_bufSize] = {};
89 QHttpMessageStreamParser m_streamReader;
90 /*!
91 \internal
92 Indicates if the current message is not read out entirely.
93 */
94 bool m_isReading = true;
95 /*!
96 \internal
97 Indicates if an EOF was encountered. No more data can be read after an EOF.
98 */
99 bool m_hasEof = false;
100 /*!
101 \internal
102 Indicates whether sendData() should be called or not.
103 */
104 bool m_shouldSendData = false;
105signals:
106 void receivedData(const QByteArray &data, bool canRequestMoreData);
107 void eof();
108public slots:
109 void readNextMessage()
110 {
111 if (m_hasEof)
112 return;
113 m_isReading = true;
114 // Try to fill up the buffer as much as possible before calling the queued signal:
115 // each loop iteration might read only one character from std::in in the worstcase, this
116 // happens for example on macos.
117 while (m_isReading) {
118 // block while waiting for some data
119 if (!std::cin.get(c&: m_buffer[m_bytesInBuf])) {
120 m_hasEof = true;
121 emit eof();
122 return;
123 }
124 // see if more data is available and fill the buffer with it
125 qsizetype readNow = std::cin.readsome(s: m_buffer + m_bytesInBuf + 1, n: s_bufSize) + 1;
126 QByteArray toAdd(m_buffer + m_bytesInBuf, readNow);
127 m_bytesInBuf += readNow;
128 m_streamReader.receiveData(data: std::move(toAdd));
129
130 m_shouldSendData |= m_bytesInBuf >= s_bufSize;
131 if (std::exchange(obj&: m_shouldSendData, new_val: false))
132 sendData();
133 }
134 }
135};
136
137static QStringList collectImportPaths(const QCommandLineParser &parser,
138 const QCommandLineOption &qmlImportPathOption,
139 const QCommandLineOption &environmentOption,
140 const QCommandLineOption &qmlImportNoDefault)
141{
142 QStringList importPaths;
143 if (parser.isSet(option: qmlImportPathOption)) {
144 const QStringList pathsFromOption =
145 QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, option: qmlImportPathOption);
146 qInfo().nospace().noquote() << "Using import directories passed by -I: \""
147 << pathsFromOption.join(sep: u"\", \""_s) << "\".";
148 importPaths << pathsFromOption;
149 }
150 if (parser.isSet(option: environmentOption)) {
151 if (const QStringList dirsFromEnv =
152 QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(environmentVariableName: u"QML_IMPORT_PATH"_s);
153 !dirsFromEnv.isEmpty()) {
154 qInfo().nospace().noquote()
155 << "Using import directories passed from environment variable "
156 "\"QML_IMPORT_PATH\": \""
157 << dirsFromEnv.join(sep: u"\", \""_s) << "\".";
158 importPaths << dirsFromEnv;
159 }
160
161 if (const QStringList dirsFromEnv2 =
162 QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(environmentVariableName: u"QML2_IMPORT_PATH"_s);
163 !dirsFromEnv2.isEmpty()) {
164 qInfo().nospace().noquote()
165 << "Using import directories passed from the deprecated environment variable "
166 "\"QML2_IMPORT_PATH\": \""
167 << dirsFromEnv2.join(sep: u"\", \""_s) << "\".";
168 importPaths << dirsFromEnv2;
169 }
170 }
171
172 // add as default fallback at the end
173 if (!parser.isSet(option: qmlImportNoDefault))
174 importPaths << QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath);
175 return importPaths;
176}
177
178// To debug:
179//
180// * simple logging can be redirected to a file
181// passing -l <file> to the qmlls command
182//
183// * more complex debugging can use named pipes:
184//
185// mkfifo qmllsIn
186// mkfifo qmllsOut
187//
188// this together with a qmllsEcho script that can be defined as
189//
190// #!/bin/sh
191// cat -u < ~/qmllsOut &
192// cat -u > ~/qmllsIn
193//
194// allows to use qmllsEcho as lsp server, and still easily start
195// it in a terminal
196//
197// qmlls < ~/qmllsIn > ~/qmllsOut
198//
199// * statup can be slowed down to have the time to attach via the
200// -w <nSeconds> flag.
201
202int qmllsMain(int argv, char *argc[])
203{
204#ifdef Q_OS_WIN32
205 // windows does not open stdin/stdout in binary mode by default
206 int err = _setmode(_fileno(stdout), _O_BINARY);
207 if (err == -1)
208 perror("Cannot set mode for stdout");
209 err = _setmode(_fileno(stdin), _O_BINARY);
210 if (err == -1)
211 perror("Cannot set mode for stdin");
212#endif
213
214 QHashSeed::setDeterministicGlobalSeed();
215 QCoreApplication app(argv, argc);
216
217 QCommandLineParser parser;
218 QQmlToolingSettings settings("qmlls"_L1);
219 parser.setApplicationDescription("QML languageserver"_L1);
220
221 parser.addHelpOption();
222 QCommandLineOption waitOption(QStringList() << "w"_L1
223 << "wait"_L1,
224 "Waits the given number of seconds before startup"_L1,
225 "waitSeconds"_L1);
226 parser.addOption(commandLineOption: waitOption);
227
228 QCommandLineOption verboseOption(
229 QStringList() << "v"_L1
230 << "verbose"_L1,
231 "Outputs extra information on the operations being performed"_L1);
232 parser.addOption(commandLineOption: verboseOption);
233
234 QCommandLineOption logFileOption(QStringList() << "l"_L1
235 << "log-file"_L1,
236 "Writes logging to the given file"_L1, "logFile"_L1);
237 parser.addOption(commandLineOption: logFileOption);
238
239 QString buildDir = QStringLiteral(u"buildDir");
240 QCommandLineOption buildDirOption(QStringList() << "b"_L1
241 << "build-dir"_L1,
242 "Adds a build dir to look up for qml information"_L1,
243 buildDir);
244 parser.addOption(commandLineOption: buildDirOption);
245 settings.addOption(name: buildDir);
246
247 QString qmlImportPath = QStringLiteral(u"importPaths");
248 QCommandLineOption qmlImportPathOption(QStringList() << "I"_L1,
249 "Look for QML modules in the specified directory"_L1,
250 qmlImportPath);
251 parser.addOption(commandLineOption: qmlImportPathOption);
252 settings.addOption(name: qmlImportPath);
253
254 QCommandLineOption environmentOption(
255 QStringList() << "E"_L1,
256 "Use the QML_IMPORT_PATH environment variable to look for QML Modules"_L1);
257 parser.addOption(commandLineOption: environmentOption);
258
259 QCommandLineOption writeDefaultsOption(
260 QStringList() << "write-defaults"_L1,
261 "Writes defaults settings to .qmlls.ini and exits (Warning: This "
262 "will overwrite any existing settings and comments!)"_L1);
263 parser.addOption(commandLineOption: writeDefaultsOption);
264
265 QCommandLineOption ignoreSettings(QStringList() << "ignore-settings"_L1,
266 "Ignores all settings files and only takes "
267 "command line options into consideration"_L1);
268 parser.addOption(commandLineOption: ignoreSettings);
269
270 QCommandLineOption noCMakeCallsOption(
271 QStringList() << "no-cmake-calls"_L1,
272 "Disables automatic CMake rebuilds and C++ file watching."_L1);
273 parser.addOption(commandLineOption: noCMakeCallsOption);
274 settings.addOption(name: "no-cmake-calls"_L1, defaultValue: "false"_L1);
275
276 QCommandLineOption docDir({ { "d"_L1, "p"_L1, "doc-dir"_L1 },
277 "Documentation path to use for the documentation hints feature"_L1,
278 "path"_L1,
279 QString() });
280 parser.addOption(commandLineOption: docDir);
281 settings.addOption(name: "docDir"_L1);
282
283 QCommandLineOption qmlImportNoDefault(QStringList() << "bare"_L1,
284 "Do not include default import directories. "
285 "This may be used to run qmlls on a Boot2Qt project."_L1);
286 parser.addOption(commandLineOption: qmlImportNoDefault);
287 const QString qmlImportNoDefaultSetting = "DisableDefaultImports"_L1;
288 settings.addOption(name: qmlImportNoDefaultSetting, defaultValue: false);
289
290 // we can't use parser.addVersionOption() because we already have one '-v' option for verbose...
291 QCommandLineOption versionOption("version"_L1, "Displays version information."_L1);
292 parser.addOption(commandLineOption: versionOption);
293
294 parser.process(app);
295
296 if (parser.isSet(option: versionOption)) {
297 parser.showVersion();
298 return EXIT_SUCCESS;
299 }
300
301 if (parser.isSet(option: writeDefaultsOption)) {
302 return settings.writeDefaults() ? 0 : 1;
303 }
304 if (parser.isSet(option: logFileOption)) {
305 QString fileName = parser.value(option: logFileOption);
306 qInfo() << "will log to" << fileName;
307 logFile = new QFile(fileName);
308 logFileLock = new QMutex;
309 if (!logFile->open(flags: QFile::WriteOnly | QFile::Truncate | QFile::Text)) {
310 qWarning(msg: "Failed to open file %s: %s", qPrintable(logFile->fileName()),
311 qPrintable(logFile->errorString()));
312 }
313 qInstallMessageHandler([](QtMsgType t, const QMessageLogContext &, const QString &msg) {
314 QMutexLocker l(logFileLock);
315 logFile->write(data: QString::number(int(t)).toUtf8());
316 logFile->write(data: " ");
317 logFile->write(data: msg.toUtf8());
318 logFile->write(data: "\n");
319 logFile->flush();
320 });
321 }
322 if (parser.isSet(option: verboseOption))
323 QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n"_L1);
324 if (parser.isSet(option: waitOption)) {
325 int waitSeconds = parser.value(option: waitOption).toInt();
326 if (waitSeconds > 0)
327 qDebug() << "waiting";
328 QThread::sleep(waitSeconds);
329 qDebug() << "starting";
330 }
331 QMutex writeMutex;
332 QQmlLanguageServer qmlServer(
333 [&writeMutex](const QByteArray &data) {
334 QMutexLocker l(&writeMutex);
335 std::cout.write(s: data.constData(), n: data.size());
336 std::cout.flush();
337 },
338 (parser.isSet(option: ignoreSettings) ? nullptr : &settings));
339
340 if (parser.isSet(option: docDir))
341 qmlServer.codeModel()->setDocumentationRootPath(
342 QString::fromUtf8(ba: parser.value(option: docDir).toUtf8()));
343
344 const bool disableCMakeCallsViaEnvironment =
345 qmlGetConfigOption<bool, qmlConvertBoolConfigOption>(var: "QMLLS_NO_CMAKE_CALLS");
346
347 if (disableCMakeCallsViaEnvironment || parser.isSet(option: noCMakeCallsOption)) {
348 if (disableCMakeCallsViaEnvironment) {
349 qWarning() << "Disabling CMake calls via QMLLS_NO_CMAKE_CALLS environment variable.";
350 } else {
351 qWarning() << "Disabling CMake calls via command line switch.";
352 }
353
354 qmlServer.codeModel()->disableCMakeCalls();
355 }
356
357 if (parser.isSet(option: buildDirOption)) {
358 const QStringList dirs =
359 QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, option: buildDirOption);
360
361 qInfo().nospace().noquote()
362 << "Using build directories passed by -b: \"" << dirs.join(sep: u"\", \""_s) << "\".";
363
364 qmlServer.codeModel()->setBuildPathsForRootUrl(url: QByteArray(), paths: dirs);
365 } else if (QStringList dirsFromEnv =
366 QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(environmentVariableName: u"QMLLS_BUILD_DIRS"_s);
367 !dirsFromEnv.isEmpty()) {
368
369 // warn now at qmlls startup that those directories will be used later in qqmlcodemodel when
370 // searching for build folders.
371 qInfo().nospace().noquote() << "Using build directories passed from environment variable "
372 "\"QMLLS_BUILD_DIRS\": \""
373 << dirsFromEnv.join(sep: u"\", \""_s) << "\".";
374
375 } else {
376 qInfo() << "Using the build directories found in the .qmlls.ini file. Your build folder "
377 "might not be found if no .qmlls.ini files are present in the root source "
378 "folder.";
379 }
380
381 qmlServer.codeModel()->setImportPaths(
382 collectImportPaths(parser, qmlImportPathOption, environmentOption, qmlImportNoDefault));
383
384 StdinReader r;
385 QThread workerThread;
386 r.moveToThread(thread: &workerThread);
387 QObject::connect(sender: &r, signal: &StdinReader::receivedData, context: qmlServer.server(),
388 slot: &QLanguageServer::receiveData);
389 QObject::connect(sender: qmlServer.server(), signal: &QLanguageServer::readNextMessage, context: &r,
390 slot: &StdinReader::readNextMessage);
391 auto exit = [&app, &workerThread]() {
392 workerThread.quit();
393 workerThread.wait();
394 QTimer::singleShot(interval: 100, receiver: &app, slot: []() {
395 QCoreApplication::processEvents();
396 QCoreApplication::exit();
397 });
398 };
399 QObject::connect(sender: &r, signal: &StdinReader::eof, context: &app, slot&: exit);
400 QObject::connect(sender: qmlServer.server(), signal: &QLanguageServer::exit, slot&: exit);
401
402 emit r.readNextMessage();
403 workerThread.start();
404 app.exec();
405 workerThread.quit();
406 workerThread.wait();
407 return qmlServer.returnValue();
408}
409
410} // namespace QmlLsp
411
412QT_END_NAMESPACE
413
414#include <qmllsmain.moc>
415

source code of qtdeclarative/src/qmlls/qmllsmain.cpp