// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_CONFIG(commandlineparser) # include #endif #ifndef QT_BOOTSTRAPPED # include #endif #include #ifdef Q_OS_WIN32 # include # include #endif using namespace QmlLsp; QFile *logFile = nullptr; QBasicMutex *logFileLock = nullptr; class StdinReader : public QObject { Q_OBJECT public: void run() { auto guard = qScopeGuard([this]() { emit eof(); }); const constexpr qsizetype bufSize = 1024; qsizetype bytesInBuf = 0; char bufferData[2 * bufSize]; char *buffer = static_cast(bufferData); auto trySend = [this, &bytesInBuf, buffer]() { if (bytesInBuf == 0) return; qsizetype toSend = bytesInBuf; bytesInBuf = 0; QByteArray dataToSend(buffer, toSend); emit receivedData(dataToSend); }; QHttpMessageStreamParser streamParser( [](const QByteArray &, const QByteArray &) { /* just a header, do nothing */ }, [&trySend](const QByteArray &) { // message body trySend(); }, [&trySend](QtMsgType, QString) { // there was an error trySend(); }, QHttpMessageStreamParser::UNBUFFERED); while (std::cin.get(buffer[bytesInBuf])) { // should poll/select and process events qsizetype readNow = std::cin.readsome(buffer + bytesInBuf + 1, bufSize) + 1; QByteArray toAdd(buffer + bytesInBuf, readNow); bytesInBuf += readNow; if (bytesInBuf >= bufSize) trySend(); streamParser.receiveData(toAdd); } trySend(); } signals: void receivedData(const QByteArray &data); void eof(); }; // To debug: // // * simple logging can be redirected to a file // passing -l to the qmlls command // // * more complex debugging can use named pipes: // // mkfifo qmllsIn // mkfifo qmllsOut // // this together with a qmllsEcho script that can be defined as // // #!/bin/sh // cat -u < ~/qmllsOut & // cat -u > ~/qmllsIn // // allows to use qmllsEcho as lsp server, and still easily start // it in a terminal // // qmlls < ~/qmllsIn > ~/qmllsOut // // * statup can be slowed down to have the time to attach via the // -w flag. int main(int argv, char *argc[]) { #ifdef Q_OS_WIN32 // windows does not open stdin/stdout in binary mode by default int err = _setmode(_fileno(stdout), _O_BINARY); if (err == -1) perror("Cannot set mode for stdout"); err = _setmode(_fileno(stdin), _O_BINARY); if (err == -1) perror("Cannot set mode for stdin"); #endif QHashSeed::setDeterministicGlobalSeed(); QCoreApplication app(argv, argc); QCoreApplication::setApplicationName("qmlls"); QCoreApplication::setApplicationVersion(QT_VERSION_STR); QCommandLineParser parser; QQmlToolingSettings settings(QLatin1String("qmlls")); parser.setApplicationDescription(QLatin1String(R"(QML languageserver)")); parser.addHelpOption(); QCommandLineOption waitOption(QStringList() << "w" << "wait", QLatin1String("Waits the given number of seconds before startup"), QLatin1String("waitSeconds")); parser.addOption(waitOption); QCommandLineOption verboseOption( QStringList() << "v" << "verbose", QLatin1String("Outputs extra information on the operations being performed")); parser.addOption(verboseOption); QCommandLineOption logFileOption(QStringList() << "l" << "log-file", QLatin1String("Writes logging to the given file"), QLatin1String("logFile")); parser.addOption(logFileOption); QString buildDir = QStringLiteral(u"buildDir"); QCommandLineOption buildDirOption( QStringList() << "b" << "build-dir", QLatin1String("Adds a build dir to look up for qml information"), buildDir); parser.addOption(buildDirOption); settings.addOption(buildDir); QString qmlImportPath = QStringLiteral(u"qml-import-path"); QCommandLineOption qmlImportPathOption( QStringList() << "I", QLatin1String("Look for QML modules in the specified directory"), qmlImportPath); parser.addOption(qmlImportPathOption); QCommandLineOption environmentOption( QStringList() << "E", QLatin1String("Use the QML_IMPORT_PATH environment variable to look for QML Modules")); parser.addOption(environmentOption); QCommandLineOption writeDefaultsOption( QStringList() << "write-defaults", QLatin1String("Writes defaults settings to .qmlls.ini and exits (Warning: This " "will overwrite any existing settings and comments!)")); parser.addOption(writeDefaultsOption); QCommandLineOption ignoreSettings(QStringList() << "ignore-settings", QLatin1String("Ignores all settings files and only takes " "command line options into consideration")); parser.addOption(ignoreSettings); QCommandLineOption noCMakeCallsOption( QStringList() << "no-cmake-calls", QLatin1String("Disables automatic CMake rebuilds and C++ file watching.")); parser.addOption(noCMakeCallsOption); settings.addOption("no-cmake-calls", "false"); parser.process(app); if (parser.isSet(writeDefaultsOption)) { return settings.writeDefaults() ? 0 : 1; } if (parser.isSet(logFileOption)) { QString fileName = parser.value(logFileOption); qInfo() << "will log to" << fileName; logFile = new QFile(fileName); logFileLock = new QMutex; logFile->open(QFile::WriteOnly | QFile::Truncate | QFile::Text); qInstallMessageHandler([](QtMsgType t, const QMessageLogContext &, const QString &msg) { QMutexLocker l(logFileLock); logFile->write(QString::number(int(t)).toUtf8()); logFile->write(" "); logFile->write(msg.toUtf8()); logFile->write("\n"); logFile->flush(); }); } if (parser.isSet(verboseOption)) QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n"); if (parser.isSet(waitOption)) { int waitSeconds = parser.value(waitOption).toInt(); if (waitSeconds > 0) qDebug() << "waiting"; QThread::sleep(waitSeconds); qDebug() << "starting"; } QMutex writeMutex; QQmlLanguageServer qmlServer( [&writeMutex](const QByteArray &data) { QMutexLocker l(&writeMutex); std::cout.write(data.constData(), data.size()); std::cout.flush(); }, (parser.isSet(ignoreSettings) ? nullptr : &settings)); const bool disableCMakeCallsViaEnvironment = qmlGetConfigOption("QMLLS_NO_CMAKE_CALLS"); if (disableCMakeCallsViaEnvironment || parser.isSet(noCMakeCallsOption)) { if (disableCMakeCallsViaEnvironment) { qWarning() << "Disabling CMake calls via QMLLS_NO_CMAKE_CALLS environment variable."; } else { qWarning() << "Disabling CMake calls via command line switch."; } qmlServer.codeModel()->disableCMakeCalls(); } if (parser.isSet(buildDirOption)) { const QStringList dirs = QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, buildDirOption); qInfo().nospace().noquote() << "Using build directories passed by -b: \"" << dirs.join(u"\", \""_s) << "\"."; qmlServer.codeModel()->setBuildPathsForRootUrl(QByteArray(), dirs); } else if (QStringList dirsFromEnv = QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv("QMLLS_BUILD_DIRS"); !dirsFromEnv.isEmpty()) { // warn now at qmlls startup that those directories will be used later in qqmlcodemodel when // searching for build folders. qInfo().nospace().noquote() << "Using build directories passed from environment variable " "\"QMLLS_BUILD_DIRS\": \"" << dirsFromEnv.join(u"\", \""_s) << "\"."; } else { qInfo() << "Using the build directories found in the .qmlls.ini file. Your build folder " "might not be found if no .qmlls.ini files are present in the root source " "folder."; } QStringList importPaths{ QLibraryInfo::path(QLibraryInfo::QmlImportsPath) }; if (parser.isSet(qmlImportPathOption)) { const QStringList pathsFromOption = QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, qmlImportPathOption); qInfo().nospace().noquote() << "Using import directories passed by -I: \"" << pathsFromOption.join(u"\", \""_s) << "\"."; importPaths << pathsFromOption; } if (parser.isSet(environmentOption)) { if (const QStringList dirsFromEnv = QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML_IMPORT_PATH"_s); !dirsFromEnv.isEmpty()) { qInfo().nospace().noquote() << "Using import directories passed from environment variable " "\"QML_IMPORT_PATH\": \"" << dirsFromEnv.join(u"\", \""_s) << "\"."; importPaths << dirsFromEnv; } if (const QStringList dirsFromEnv2 = QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML2_IMPORT_PATH"_s); !dirsFromEnv2.isEmpty()) { qInfo().nospace().noquote() << "Using import directories passed from the deprecated environment variable " "\"QML2_IMPORT_PATH\": \"" << dirsFromEnv2.join(u"\", \""_s) << "\"."; importPaths << dirsFromEnv2; } } qmlServer.codeModel()->setImportPaths(importPaths); StdinReader r; QObject::connect(&r, &StdinReader::receivedData, qmlServer.server(), &QLanguageServer::receiveData); QObject::connect(&r, &StdinReader::eof, &app, [&app]() { QTimer::singleShot(100, &app, []() { QCoreApplication::processEvents(); QCoreApplication::exit(); }); }); QThreadPool::globalInstance()->start([&r]() { r.run(); }); app.exec(); return qmlServer.returnValue(); } #include "qmllanguageservertool.moc"