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

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

source code of qtdeclarative/tools/qmlls/qmllanguageservertool.cpp