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 <QtCore/qdiriterator.h> |
12 | #include <QtCore/qjsonobject.h> |
13 | #include <QtCore/qjsonarray.h> |
14 | #include <QtCore/qjsondocument.h> |
15 | #include <QtCore/qmutex.h> |
16 | #include <QtCore/QMutexLocker> |
17 | #include <QtCore/qscopedpointer.h> |
18 | #include <QtCore/qrunnable.h> |
19 | #include <QtCore/qthreadpool.h> |
20 | #include <QtCore/qtimer.h> |
21 | |
22 | #include <QtJsonRpc/private/qhttpmessagestreamparser_p.h> |
23 | |
24 | #include <QtQmlCompiler/private/qqmljsresourcefilemapper_p.h> |
25 | #include <QtQmlCompiler/private/qqmljscompiler_p.h> |
26 | #include <QtQmlCompiler/private/qqmljslogger_p.h> |
27 | #include <QtQmlCompiler/private/qqmljsscope_p.h> |
28 | #include <QtQmlCompiler/private/qqmljsimporter_p.h> |
29 | #if QT_CONFIG(commandlineparser) |
30 | # include <QtCore/qcommandlineparser.h> |
31 | #endif |
32 | |
33 | #ifndef QT_BOOTSTRAPPED |
34 | # include <QtCore/qlibraryinfo.h> |
35 | #endif |
36 | |
37 | #include <iostream> |
38 | #ifdef Q_OS_WIN32 |
39 | # include <fcntl.h> |
40 | # include <io.h> |
41 | #endif |
42 | |
43 | using namespace QmlLsp; |
44 | |
45 | QFile *logFile = nullptr; |
46 | QBasicMutex *logFileLock = nullptr; |
47 | |
48 | class StdinReader : public QObject |
49 | { |
50 | Q_OBJECT |
51 | public: |
52 | void run() |
53 | { |
54 | auto guard = qScopeGuard(f: [this]() { emit eof(); }); |
55 | const constexpr qsizetype bufSize = 1024; |
56 | qsizetype bytesInBuf = 0; |
57 | char bufferData[2 * bufSize]; |
58 | char *buffer = static_cast<char *>(bufferData); |
59 | |
60 | auto trySend = [this, &bytesInBuf, buffer]() { |
61 | if (bytesInBuf == 0) |
62 | return; |
63 | qsizetype toSend = bytesInBuf; |
64 | bytesInBuf = 0; |
65 | QByteArray dataToSend(buffer, toSend); |
66 | emit receivedData(data: dataToSend); |
67 | }; |
68 | QHttpMessageStreamParser streamParser( |
69 | [](const QByteArray &, const QByteArray &) { /* just a header, do nothing */ }, |
70 | [&trySend](const QByteArray &) { |
71 | // message body |
72 | trySend(); |
73 | }, |
74 | [&trySend](QtMsgType, QString) { |
75 | // there was an error |
76 | trySend(); |
77 | }, |
78 | QHttpMessageStreamParser::UNBUFFERED); |
79 | |
80 | while (std::cin.get(c&: buffer[bytesInBuf])) { // should poll/select and process events |
81 | qsizetype readNow = std::cin.readsome(s: buffer + bytesInBuf + 1, n: bufSize) + 1; |
82 | QByteArray toAdd(buffer + bytesInBuf, readNow); |
83 | bytesInBuf += readNow; |
84 | if (bytesInBuf >= bufSize) |
85 | trySend(); |
86 | streamParser.receiveData(data: toAdd); |
87 | } |
88 | trySend(); |
89 | } |
90 | signals: |
91 | void receivedData(const QByteArray &data); |
92 | void eof(); |
93 | }; |
94 | |
95 | // To debug: |
96 | // |
97 | // * simple logging can be redirected to a file |
98 | // passing -l <file> to the qmlls command |
99 | // |
100 | // * more complex debugging can use named pipes: |
101 | // |
102 | // mkfifo qmllsIn |
103 | // mkfifo qmllsOut |
104 | // |
105 | // this together with a qmllsEcho script that can be defined as |
106 | // |
107 | // #!/bin/sh |
108 | // cat -u < ~/qmllsOut & |
109 | // cat -u > ~/qmllsIn |
110 | // |
111 | // allows to use qmllsEcho as lsp server, and still easily start |
112 | // it in a terminal |
113 | // |
114 | // qmlls < ~/qmllsIn > ~/qmllsOut |
115 | // |
116 | // * statup can be slowed down to have the time to attach via the |
117 | // -w <nSeconds> flag. |
118 | |
119 | int main(int argv, char *argc[]) |
120 | { |
121 | #ifdef Q_OS_WIN32 |
122 | // windows does not open stdin/stdout in binary mode by default |
123 | int err = _setmode(_fileno(stdout), _O_BINARY); |
124 | if (err == -1) |
125 | perror("Cannot set mode for stdout" ); |
126 | err = _setmode(_fileno(stdin), _O_BINARY); |
127 | if (err == -1) |
128 | perror("Cannot set mode for stdin" ); |
129 | #endif |
130 | |
131 | QHashSeed::setDeterministicGlobalSeed(); |
132 | QCoreApplication app(argv, argc); |
133 | QCoreApplication::setApplicationName("qmlls" ); |
134 | QCoreApplication::setApplicationVersion(QT_VERSION_STR); |
135 | |
136 | QCommandLineParser parser; |
137 | QQmlToolingSettings settings(QLatin1String("qmlls" )); |
138 | parser.setApplicationDescription(QLatin1String(R"(QML languageserver)" )); |
139 | |
140 | parser.addHelpOption(); |
141 | QCommandLineOption waitOption(QStringList() << "w" |
142 | << "wait" , |
143 | QLatin1String("Waits the given number of seconds before startup" ), |
144 | QLatin1String("waitSeconds" )); |
145 | parser.addOption(commandLineOption: waitOption); |
146 | |
147 | QCommandLineOption verboseOption( |
148 | QStringList() << "v" |
149 | << "verbose" , |
150 | QLatin1String("Outputs extra information on the operations being performed" )); |
151 | parser.addOption(commandLineOption: verboseOption); |
152 | |
153 | QCommandLineOption logFileOption(QStringList() << "l" |
154 | << "log-file" , |
155 | QLatin1String("Writes logging to the given file" ), |
156 | QLatin1String("logFile" )); |
157 | parser.addOption(commandLineOption: logFileOption); |
158 | |
159 | QString buildDir = QStringLiteral(u"buildDir" ); |
160 | QCommandLineOption buildDirOption( |
161 | QStringList() << "b" |
162 | << "build-dir" , |
163 | QLatin1String("Adds a build dir to look up for qml information" ), buildDir); |
164 | parser.addOption(commandLineOption: buildDirOption); |
165 | settings.addOption(name: buildDir); |
166 | |
167 | QCommandLineOption writeDefaultsOption( |
168 | QStringList() << "write-defaults" , |
169 | QLatin1String("Writes defaults settings to .qmlls.ini and exits (Warning: This " |
170 | "will overwrite any existing settings and comments!)" )); |
171 | parser.addOption(commandLineOption: writeDefaultsOption); |
172 | |
173 | QCommandLineOption ignoreSettings(QStringList() << "ignore-settings" , |
174 | QLatin1String("Ignores all settings files and only takes " |
175 | "command line options into consideration" )); |
176 | parser.addOption(commandLineOption: ignoreSettings); |
177 | |
178 | parser.process(app); |
179 | |
180 | if (parser.isSet(option: writeDefaultsOption)) { |
181 | return settings.writeDefaults() ? 0 : 1; |
182 | } |
183 | if (parser.isSet(option: logFileOption)) { |
184 | QString fileName = parser.value(option: logFileOption); |
185 | qInfo() << "will log to" << fileName; |
186 | logFile = new QFile(fileName); |
187 | logFileLock = new QMutex; |
188 | logFile->open(flags: QFile::WriteOnly | QFile::Truncate | QFile::Text); |
189 | qInstallMessageHandler([](QtMsgType t, const QMessageLogContext &, const QString &msg) { |
190 | QMutexLocker l(logFileLock); |
191 | logFile->write(data: QString::number(int(t)).toUtf8()); |
192 | logFile->write(data: " " ); |
193 | logFile->write(data: msg.toUtf8()); |
194 | logFile->write(data: "\n" ); |
195 | logFile->flush(); |
196 | }); |
197 | } |
198 | if (parser.isSet(option: verboseOption)) |
199 | QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n" ); |
200 | if (parser.isSet(option: waitOption)) { |
201 | int waitSeconds = parser.value(option: waitOption).toInt(); |
202 | if (waitSeconds > 0) |
203 | qDebug() << "waiting" ; |
204 | QThread::sleep(waitSeconds); |
205 | qDebug() << "starting" ; |
206 | } |
207 | QMutex writeMutex; |
208 | QQmlLanguageServer qmlServer( |
209 | [&writeMutex](const QByteArray &data) { |
210 | QMutexLocker l(&writeMutex); |
211 | std::cout.write(s: data.constData(), n: data.size()); |
212 | std::cout.flush(); |
213 | }, |
214 | (parser.isSet(option: ignoreSettings) ? nullptr : &settings)); |
215 | |
216 | const QStringList envPaths = |
217 | qEnvironmentVariable(varName: "QMLLS_BUILD_DIRS" ).split(sep: u',', behavior: Qt::SkipEmptyParts); |
218 | for (const QString &envPath : envPaths) { |
219 | QFileInfo info(envPath); |
220 | if (!info.exists()) { |
221 | qWarning() << "Argument" << buildDir << "passed via QMLLS_BUILD_DIRS does not exist." ; |
222 | } else if (!info.isDir()) { |
223 | qWarning() << "Argument" << buildDir |
224 | << "passed via QMLLS_BUILD_DIRS is not a directory." ; |
225 | } |
226 | } |
227 | |
228 | QStringList buildDirs; |
229 | if (parser.isSet(option: buildDirOption)) { |
230 | buildDirs = parser.values(option: buildDirOption); |
231 | for (const QString &buildDir : buildDirs) { |
232 | QFileInfo info(buildDir); |
233 | if (!info.exists()) { |
234 | qWarning() << "Argument" << buildDir << "passed to --build-dir does not exist." ; |
235 | } else if (!info.isDir()) { |
236 | qWarning() << "Argument" << buildDir << "passed to --build-dir is not a directory." ; |
237 | } |
238 | } |
239 | qmlServer.codeModel()->setBuildPathsForRootUrl(url: QByteArray(), paths: buildDirs); |
240 | } |
241 | |
242 | if (!buildDirs.isEmpty()) { |
243 | qInfo() << "Using the build directories passed via the --build-dir option:" |
244 | << buildDirs.join(sep: ", " ); |
245 | } else if (!envPaths.isEmpty()) { |
246 | qInfo() << "Using the build directories passed via the QMLLS_BUILD_DIRS environment " |
247 | "variable" |
248 | << buildDirs.join(sep: ", " ); |
249 | } else { |
250 | qInfo() << "Using the build directories found in the .qmlls.ini file. Your build folder " |
251 | "might not be found if no .qmlls.ini files are present in the root source " |
252 | "folder." ; |
253 | } |
254 | |
255 | if (buildDirs.isEmpty() && envPaths.isEmpty()) { |
256 | qInfo() << "Build directory path omitted: Your source folders will be searched for " |
257 | ".qmlls.ini files." ; |
258 | } |
259 | StdinReader r; |
260 | QObject::connect(sender: &r, signal: &StdinReader::receivedData, |
261 | context: qmlServer.server(), slot: &QLanguageServer::receiveData); |
262 | QObject::connect(sender: &r, signal: &StdinReader::eof, context: &app, slot: [&app]() { |
263 | QTimer::singleShot(interval: 100, receiver: &app, slot: []() { |
264 | QCoreApplication::processEvents(); |
265 | QCoreApplication::exit(); |
266 | }); |
267 | }); |
268 | QThreadPool::globalInstance()->start(functionToRun: [&r]() { r.run(); }); |
269 | app.exec(); |
270 | return qmlServer.returnValue(); |
271 | } |
272 | |
273 | #include "qmllanguageservertool.moc" |
274 | |