diff options
Diffstat (limited to 'src/app')
43 files changed, 1166 insertions, 397 deletions
diff --git a/src/app/app.pri b/src/app/app.pri deleted file mode 100644 index 5cd9cb4aa..000000000 --- a/src/app/app.pri +++ /dev/null @@ -1,23 +0,0 @@ -include(../install_prefix.pri) - -QT = core -TEMPLATE = app -DEFINES += QT_NO_CAST_FROM_ASCII QT_NO_PROCESS_COMBINED_ARGUMENT_START -!isEmpty(QBS_APPS_DESTDIR):DESTDIR = $${QBS_APPS_DESTDIR} -else:DESTDIR = ../../../bin - -!isEmpty(QBS_APPS_RPATH_DIR) { - linux-*:QMAKE_LFLAGS += -Wl,-z,origin \'-Wl,-rpath,$${QBS_APPS_RPATH_DIR}\' - macx:QMAKE_LFLAGS += -Wl,-rpath,$${QBS_APPS_RPATH_DIR} -} - -CONFIG += console -CONFIG -= app_bundle -CONFIG += c++17 - -include($${PWD}/../lib/corelib/use_corelib.pri) -include($${PWD}/shared/logging/logging.pri) - -!isEmpty(QBS_APPS_INSTALL_DIR):target.path = $${QBS_APPS_INSTALL_DIR} -else:target.path = $${QBS_INSTALL_PREFIX}/bin -INSTALLS += target diff --git a/src/app/app.pro b/src/app/app.pro deleted file mode 100644 index 935ce1776..000000000 --- a/src/app/app.pro +++ /dev/null @@ -1,10 +0,0 @@ -TEMPLATE = subdirs -SUBDIRS =\ - qbs\ - qbs-create-project \ - qbs-setup-android \ - qbs-setup-toolchains \ - qbs-setup-qt \ - config - -!isEmpty(QT.widgets.name):SUBDIRS += config-ui diff --git a/src/app/config-ui/config-ui.pro b/src/app/config-ui/config-ui.pro deleted file mode 100644 index 84d3b3e4d..000000000 --- a/src/app/config-ui/config-ui.pro +++ /dev/null @@ -1,26 +0,0 @@ -include(../app.pri) - -CONFIG -= console -QT += gui widgets - -TARGET = qbs-config-ui - -HEADERS += \ - commandlineparser.h \ - mainwindow.h - -SOURCES += \ - commandlineparser.cpp \ - main.cpp \ - mainwindow.cpp - -OTHER_FILES += \ - Info.plist - -osx { - QMAKE_LFLAGS += -sectcreate __TEXT __info_plist $$shell_quote($$PWD/Info.plist) - OBJECTIVE_SOURCES += fgapp.mm - LIBS += -framework ApplicationServices -framework Cocoa -} - -FORMS += mainwindow.ui diff --git a/src/app/config/config.pro b/src/app/config/config.pro deleted file mode 100644 index 278c481d1..000000000 --- a/src/app/config/config.pro +++ /dev/null @@ -1,13 +0,0 @@ -include(../app.pri) - -TARGET = qbs-config - -SOURCES += \ - configcommandexecutor.cpp \ - configcommandlineparser.cpp \ - configmain.cpp - -HEADERS += \ - configcommand.h \ - configcommandexecutor.h \ - configcommandlineparser.h diff --git a/src/app/config/configcommandexecutor.cpp b/src/app/config/configcommandexecutor.cpp index 023514980..cbcb5ee73 100644 --- a/src/app/config/configcommandexecutor.cpp +++ b/src/app/config/configcommandexecutor.cpp @@ -49,11 +49,52 @@ #include <QtCore/qdir.h> #include <QtCore/qfile.h> #include <QtCore/qtextstream.h> +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonobject.h> #include <cstdio> using namespace qbs; +static QJsonObject settingsToJSONObject( + Settings &settings, qbs::Settings::Scopes scopes, const QString &parentGroup = {}) +{ + QJsonObject result; + + const auto allKeys = settings.directChildren(parentGroup, scopes); + for (const auto& key : allKeys) { + const auto fullKey = parentGroup.isEmpty() + ? key + : QStringLiteral("%1.%2").arg(parentGroup, key); + const auto value = settings.value(fullKey, scopes); + if (value.isValid()) { // looks like a real value + result[key] = QJsonValue::fromVariant(value); + } else { // looks like a group + result[key] = settingsToJSONObject(settings, scopes, fullKey); + } + } + + return result; +} + +static void settingsFromJSONObject( + Settings &settings, const QJsonObject &object, const QString &parentGroup = {}) +{ + for (auto it = object.begin(), end = object.end(); it != end; ++it) { + const auto key = it.key(); + const auto value = it.value(); + const auto fullKey = parentGroup.isEmpty() + ? key + : QStringLiteral("%1.%2").arg(parentGroup, key); + if (value.isObject()) { + settingsFromJSONObject(settings, it.value().toObject(), fullKey); + } else { + settings.setValue(fullKey, value.toVariant()); + } + } +} + + ConfigCommandExecutor::ConfigCommandExecutor(Settings *settings, Settings::Scopes scope) : m_settings(settings), m_scope(scope) { @@ -139,12 +180,20 @@ void ConfigCommandExecutor::exportSettings(const QString &filename) throw ErrorInfo(tr("Could not open file '%1' for writing: %2") .arg(QDir::toNativeSeparators(filename), file.errorString())); } - QTextStream stream(&file); - setupDefaultCodec(stream); - const auto keys = m_settings->allKeys(m_scope); - for (const QString &key : keys) - stream << key << ": " << qbs::settingsValueToRepresentation(m_settings->value(key, m_scope)) - << Qt::endl; + + if (QFileInfo(filename).suffix() == u"json") { + QJsonDocument doc; + doc.setObject(settingsToJSONObject(*m_settings, m_scope)); + file.write(doc.toJson()); + } else { + QTextStream stream(&file); + setupDefaultCodec(stream); + const auto keys = m_settings->allKeys(m_scope); + for (const QString &key : keys) + stream << key << ": " + << qbs::settingsValueToRepresentation(m_settings->value(key, m_scope)) + << Qt::endl; + } } void ConfigCommandExecutor::importSettings(const QString &filename) @@ -159,15 +208,21 @@ void ConfigCommandExecutor::importSettings(const QString &filename) for (const QString &key : keys) m_settings->remove(key); - QTextStream stream(&file); - setupDefaultCodec(stream); - while (!stream.atEnd()) { - QString line = stream.readLine(); - int colon = line.indexOf(QLatin1Char(':')); - if (colon >= 0 && !line.startsWith(QLatin1Char('#'))) { - const QString key = line.left(colon).trimmed(); - const QString value = line.mid(colon + 1).trimmed(); - m_settings->setValue(key, representationToSettingsValue(value)); + if (QFileInfo(filename).suffix() == u"json") { + const auto doc = QJsonDocument::fromJson(file.readAll()); + const auto object = doc.object(); + settingsFromJSONObject(*m_settings, doc.object()); + } else { + QTextStream stream(&file); + setupDefaultCodec(stream); + while (!stream.atEnd()) { + QString line = stream.readLine(); + int colon = line.indexOf(QLatin1Char(':')); + if (colon >= 0 && !line.startsWith(QLatin1Char('#'))) { + const QString key = line.left(colon).trimmed(); + const QString value = line.mid(colon + 1).trimmed(); + m_settings->setValue(key, representationToSettingsValue(value)); + } } } } diff --git a/src/app/config/configcommandlineparser.cpp b/src/app/config/configcommandlineparser.cpp index f4d0a142f..1ad0cc4b1 100644 --- a/src/app/config/configcommandlineparser.cpp +++ b/src/app/config/configcommandlineparser.cpp @@ -126,8 +126,8 @@ void ConfigCommandLineParser::parse(const QStringList &commandLine) throw Error(Tr::tr("Profile properties must be provided.")); if (m_command.varNames.size() % 2 != 0) throw Error(Tr::tr("Profile properties must be key/value pairs.")); - for (int i = 0; i < m_command.varNames.size(); ++i) { - if (m_command.varNames.at(i).isEmpty()) + for (const auto &varName : std::as_const(m_command.varNames)) { + if (varName.isEmpty()) throw Error(Tr::tr("Property names must not be empty.")); } break; diff --git a/src/app/qbs-create-project/create-project-main.cpp b/src/app/qbs-create-project/create-project-main.cpp index bb5d1a6bc..51e7ce514 100644 --- a/src/app/qbs-create-project/create-project-main.cpp +++ b/src/app/qbs-create-project/create-project-main.cpp @@ -80,9 +80,9 @@ int main(int argc, char *argv[]) const ProjectStructure projectStructure = parser.isSet(flatOpt) ? ProjectStructure::Flat : ProjectStructure::Composite; const QStringList whiteList = parser.value(whiteListOpt).split(QLatin1Char(','), - QBS_SKIP_EMPTY_PARTS); + Qt::SkipEmptyParts); const QStringList blackList = parser.value(blackListOpt).split(QLatin1Char(','), - QBS_SKIP_EMPTY_PARTS); + Qt::SkipEmptyParts); try { ProjectCreator().run(QDir::currentPath(), projectStructure, whiteList, blackList); } catch (const ErrorInfo &e) { diff --git a/src/app/qbs-create-project/createproject.cpp b/src/app/qbs-create-project/createproject.cpp index 4167a396e..22c845994 100644 --- a/src/app/qbs-create-project/createproject.cpp +++ b/src/app/qbs-create-project/createproject.cpp @@ -124,7 +124,7 @@ void ProjectCreator::serializeProject(const ProjectCreator::Project &project) fileContents << indent << "Depends { name: \"cpp\" }\n"; } fileContents << indent << "files: [\n"; - for (const QString &fileName : qAsConst(project.fileNames)) + for (const QString &fileName : std::as_const(project.fileNames)) fileContents << indent << indent << qbs::toJSLiteral(fileName) << ",\n"; fileContents << indent << "]\n"; for (const ProjectPtr &p : project.subProjects) @@ -152,7 +152,7 @@ void ProjectCreator::addGroups(QTextStream &stream, const QDir &baseDir, << qbs::toJSLiteral(baseDir.relativeFilePath(subProject.dirPath) + QLatin1Char('/')) << '\n'; stream << indent << indent << "files: [\n"; - for (const QString &fileName : qAsConst(subProject.fileNames)) + for (const QString &fileName : std::as_const(subProject.fileNames)) stream << indent << indent << indent << qbs::toJSLiteral(fileName) << ",\n"; stream << indent << indent << "]\n"; stream << indent << "}\n"; @@ -184,7 +184,7 @@ ProjectCreator::ProductFlags ProjectCreator::getFlags(const ProjectCreator::Proj void ProjectCreator::getFlagsFromFileNames(const ProjectCreator::Project &project, ProductFlags &flags) { - for (const QString &fileName : qAsConst(project.fileNames)) { + for (const QString &fileName : std::as_const(project.fileNames)) { if (flags.testFlag(IsApp) && flags.testFlag(NeedsQt)) return; const QFileInfo fi(project.dirPath + QLatin1Char('/') + fileName); @@ -210,7 +210,7 @@ void ProjectCreator::getFlagsFromFileNames(const ProjectCreator::Project &projec void ProjectCreator::getFlagsFromFileContents(const ProjectCreator::Project &project, ProductFlags &flags) { - for (const QString &fileName : qAsConst(project.fileNames)) { + for (const QString &fileName : std::as_const(project.fileNames)) { QFile f (project.dirPath + QLatin1Char('/') + fileName); if (!f.open(QIODevice::ReadOnly)) { qDebug() << "Ignoring failure to read" << f.fileName(); diff --git a/src/app/qbs-create-project/qbs-create-project.pro b/src/app/qbs-create-project/qbs-create-project.pro deleted file mode 100644 index 1edb85214..000000000 --- a/src/app/qbs-create-project/qbs-create-project.pro +++ /dev/null @@ -1,9 +0,0 @@ -include(../app.pri) - -TARGET = qbs-create-project - -HEADERS += \ - createproject.h -SOURCES += \ - createproject.cpp \ - create-project-main.cpp diff --git a/src/app/qbs-setup-android/android-setup.cpp b/src/app/qbs-setup-android/android-setup.cpp index 329bd0052..fad24d018 100644 --- a/src/app/qbs-setup-android/android-setup.cpp +++ b/src/app/qbs-setup-android/android-setup.cpp @@ -153,7 +153,7 @@ static QtInfoPerArch getQtAndroidInfo(const QString &qtSdkDir) QDirIterator dit(qtSdkDir, nameFilters, QDir::Dirs); while (dit.hasNext()) qtDirs << dit.next(); - for (const auto &qtDir : qAsConst(qtDirs)) { + for (const auto &qtDir : std::as_const(qtDirs)) { const QtAndroidInfo info = getInfoForQtDir(qtDir); if (info.isValid()) { for (const QString &arch: info.archs) diff --git a/src/app/qbs-setup-android/qbs-setup-android.pro b/src/app/qbs-setup-android/qbs-setup-android.pro deleted file mode 100644 index b5e57761d..000000000 --- a/src/app/qbs-setup-android/qbs-setup-android.pro +++ /dev/null @@ -1,12 +0,0 @@ -include(../app.pri) - -TARGET = qbs-setup-android - -SOURCES += \ - android-setup.cpp \ - commandlineparser.cpp \ - main.cpp - -HEADERS += \ - android-setup.h \ - commandlineparser.h diff --git a/src/app/qbs-setup-qt/main.cpp b/src/app/qbs-setup-qt/main.cpp index bef95eee2..b739bff2d 100644 --- a/src/app/qbs-setup-qt/main.cpp +++ b/src/app/qbs-setup-qt/main.cpp @@ -82,7 +82,7 @@ int main(int argc, char *argv[]) QString profileName = QLatin1String("qt-") + qtEnvironment.qtVersion.toString(); if (SetupQt::checkIfMoreThanOneQtWithTheSameVersion(qtEnvironment.qtVersion, qtEnvironments)) { QStringList prefixPathParts = QFileInfo(qtEnvironment.qmakeFilePath).path() - .split(QLatin1Char('/'), QBS_SKIP_EMPTY_PARTS); + .split(QLatin1Char('/'), Qt::SkipEmptyParts); if (!prefixPathParts.empty()) profileName += QLatin1String("-") + prefixPathParts.last(); } diff --git a/src/app/qbs-setup-qt/qbs-setup-qt.pro b/src/app/qbs-setup-qt/qbs-setup-qt.pro deleted file mode 100644 index a5694d6b7..000000000 --- a/src/app/qbs-setup-qt/qbs-setup-qt.pro +++ /dev/null @@ -1,16 +0,0 @@ -include(../app.pri) - -TARGET = qbs-setup-qt - -SOURCES += \ - commandlineparser.cpp \ - main.cpp \ - setupqt.cpp - -HEADERS += \ - commandlineparser.h \ - setupqt.h - -mingw { - RC_FILE = qbs-setup-qt.rc -} diff --git a/src/app/qbs-setup-qt/setupqt.cpp b/src/app/qbs-setup-qt/setupqt.cpp index 08caec657..bac766482 100644 --- a/src/app/qbs-setup-qt/setupqt.cpp +++ b/src/app/qbs-setup-qt/setupqt.cpp @@ -156,7 +156,7 @@ static QString archFromDirName(const QString &dir) const std::string dirString = dir.toStdString(); if (!std::regex_match(dirString, match, regexp)) return {}; - const QString arch = QString::fromStdString(match[1]); + QString arch = QString::fromStdString(match[1]); if (arch == QLatin1String("32")) return QStringLiteral("x86"); if (arch == QLatin1String("64")) diff --git a/src/app/qbs-setup-toolchains/clangclprobe.cpp b/src/app/qbs-setup-toolchains/clangclprobe.cpp index c9c5a428c..272f953fc 100644 --- a/src/app/qbs-setup-toolchains/clangclprobe.cpp +++ b/src/app/qbs-setup-toolchains/clangclprobe.cpp @@ -85,7 +85,7 @@ Profile createProfileHelper( QString findClangCl() { const auto compilerName = HostOsInfo::appendExecutableSuffix(QStringLiteral("clang-cl")); - const auto compilerFromPath = findExecutable(compilerName); + auto compilerFromPath = findExecutable(compilerName); if (!compilerFromPath.isEmpty()) return compilerFromPath; @@ -121,13 +121,23 @@ void clangClProbe(Settings *settings, std::vector<Profile> &profiles) return; } - const auto clangCl = clangCls.front(); const QString architectures[] = { QStringLiteral("x86_64"), QStringLiteral("x86") }; - qbs::Internal::transform(architectures, profiles, [settings, clangCl](const auto &arch) { - const auto profileName = QStringLiteral("clang-cl-%1").arg(arch); - return createProfileHelper(settings, profileName, clangCl.toolchainInstallPath, - clangCl.vcvarsallPath, arch); }); + for (size_t index = 0; index < clangCls.size(); ++index) { + const auto &clangCl = clangCls[index]; + const QString suffix = index == 0 ? QString() : QStringLiteral("-%1").arg(index); + qbs::Internal::transform( + architectures, profiles, [settings, clangCl, suffix](const auto &arch) { + const auto profileName = QStringLiteral("clang-cl") + suffix + + QStringLiteral("-%1").arg(arch); + return createProfileHelper( + settings, + profileName, + clangCl.toolchainInstallPath, + clangCl.vcvarsallPath, + arch); + }); + } } diff --git a/src/app/qbs-setup-toolchains/dmcprobe.cpp b/src/app/qbs-setup-toolchains/dmcprobe.cpp index cbe113dfd..097b1caed 100644 --- a/src/app/qbs-setup-toolchains/dmcprobe.cpp +++ b/src/app/qbs-setup-toolchains/dmcprobe.cpp @@ -105,8 +105,7 @@ static QStringList dumpOutput(const QFileInfo &compiler, const QStringList &flag p.waitForFinished(3000); fakeIn.remove(); static QRegularExpression re(QLatin1String("\\r?\\n")); - const QStringList lines = QString::fromUtf8(p.readAllStandardOutput()).split(re); - return lines; + return QString::fromUtf8(p.readAllStandardOutput()).split(re); } static std::optional<Target> dumpDmcTarget(const QFileInfo &compiler, const QStringList &flags) diff --git a/src/app/qbs-setup-toolchains/gccprobe.cpp b/src/app/qbs-setup-toolchains/gccprobe.cpp index dda8f2274..c8c04b0cc 100644 --- a/src/app/qbs-setup-toolchains/gccprobe.cpp +++ b/src/app/qbs-setup-toolchains/gccprobe.cpp @@ -526,7 +526,7 @@ void gccProbe(Settings *settings, std::vector<Profile> &profiles, const QString std::vector<QFileInfo> candidates; const auto filters = buildCompilerNameFilters(compilerName); - for (const auto &searchPath : qAsConst(searchPaths)) { + for (const auto &searchPath : std::as_const(searchPaths)) { const QDir dir(searchPath); const QStringList fileNames = dir.entryList( filters, QDir::Files | QDir::Executable); @@ -574,7 +574,7 @@ void gccProbe(Settings *settings, std::vector<Profile> &profiles, const QString }); } - for (const auto &candidate : qAsConst(candidates)) { + for (const auto &candidate : std::as_const(candidates)) { const QString toolchainType = toolchainTypeFromCompilerName( candidate.baseName()); const QString profileName = buildProfileName(candidate); diff --git a/src/app/qbs-setup-toolchains/msvcprobe.cpp b/src/app/qbs-setup-toolchains/msvcprobe.cpp index a0fa7cee6..84d36753e 100644 --- a/src/app/qbs-setup-toolchains/msvcprobe.cpp +++ b/src/app/qbs-setup-toolchains/msvcprobe.cpp @@ -140,7 +140,7 @@ void msvcProbe(Settings *settings, std::vector<Profile> &profiles) } } - for (const WinSDK &sdk : qAsConst(winSDKs)) { + for (const WinSDK &sdk : std::as_const(winSDKs)) { qbsInfo() << Tr::tr(" Windows SDK %1 detected:\n" " installed in %2").arg(sdk.version, sdk.vcInstallPath); if (sdk.isDefault) @@ -150,7 +150,7 @@ void msvcProbe(Settings *settings, std::vector<Profile> &profiles) // 2) Installed MSVCs std::vector<MSVC> msvcs = MSVC::installedCompilers(ConsoleLogger::instance()); - for (const MSVC &msvc : qAsConst(msvcs)) { + for (const MSVC &msvc : std::as_const(msvcs)) { qbsInfo() << Tr::tr(" MSVC %1 (%2) detected in\n" " %3").arg(msvc.version, msvc.architecture, QDir::toNativeSeparators(msvc.binPath)); @@ -164,10 +164,10 @@ void msvcProbe(Settings *settings, std::vector<Profile> &profiles) qbsInfo() << Tr::tr("Detecting build environment..."); std::vector<MSVC *> msvcPtrs; - msvcPtrs.resize(winSDKs.size() + msvcs.size()); - std::transform(winSDKs.begin(), winSDKs.end(), msvcPtrs.begin(), + msvcPtrs.reserve(winSDKs.size() + msvcs.size()); + std::transform(winSDKs.begin(), winSDKs.end(), std::back_inserter(msvcPtrs), [] (WinSDK &sdk) -> MSVC * { return &sdk; }); - std::transform(msvcs.begin(), msvcs.end(), msvcPtrs.begin() + winSDKs.size(), + std::transform(msvcs.begin(), msvcs.end(), std::back_inserter(msvcPtrs), [] (MSVC &msvc) -> MSVC * { return &msvc; }); VsEnvironmentDetector envDetector; @@ -195,7 +195,7 @@ void msvcProbe(Settings *settings, std::vector<Profile> &profiles) // a new group of compilers of the same version incrementing the set size msvcVersions.insert(msvc.vcInstallPath); // index is the number of specific vcInstallPaths (e.g. compiler versions) seen so far - const qsizetype index = msvcVersions.size() - 1; + const size_t index = msvcVersions.size() - 1; const QString suffix = index == 0 ? QString() : QStringLiteral("-%1").arg(index); const QString name = QLatin1String("MSVC") + msvc.version + suffix + QLatin1Char('-') + msvc.architecture; diff --git a/src/app/qbs-setup-toolchains/qbs-setup-toolchains.pro b/src/app/qbs-setup-toolchains/qbs-setup-toolchains.pro deleted file mode 100644 index 69d6552ee..000000000 --- a/src/app/qbs-setup-toolchains/qbs-setup-toolchains.pro +++ /dev/null @@ -1,36 +0,0 @@ -include(../app.pri) - -TARGET = qbs-setup-toolchains - -HEADERS += \ - clangclprobe.h \ - commandlineparser.h \ - cosmicprobe.h \ - dmcprobe.h \ - gccprobe.h \ - iarewprobe.h \ - keilprobe.h \ - msvcprobe.h \ - probe.h \ - sdccprobe.h \ - watcomprobe.h \ - xcodeprobe.h \ - -SOURCES += \ - clangclprobe.cpp \ - commandlineparser.cpp \ - cosmicprobe.cpp \ - dmcprobe.cpp \ - gccprobe.cpp \ - iarewprobe.cpp \ - keilprobe.cpp \ - main.cpp \ - msvcprobe.cpp \ - probe.cpp \ - sdccprobe.cpp \ - watcomprobe.cpp \ - xcodeprobe.cpp \ - -mingw { - RC_FILE = qbs-setup-toolchains.rc -} diff --git a/src/app/qbs-setup-toolchains/sdccprobe.cpp b/src/app/qbs-setup-toolchains/sdccprobe.cpp index b53615218..25a3d9751 100644 --- a/src/app/qbs-setup-toolchains/sdccprobe.cpp +++ b/src/app/qbs-setup-toolchains/sdccprobe.cpp @@ -84,8 +84,7 @@ static QStringList dumpOutput(const QFileInfo &compiler, const QString &targetFl } static QRegularExpression re(QStringLiteral("\\r?\\n")); - const QStringList lines = QString::fromUtf8(p.readAllStandardOutput()).split(re); - return lines; + return QString::fromUtf8(p.readAllStandardOutput()).split(re); } static bool supportsSdccArchitecture(const QFileInfo &compiler, QStringView flag) diff --git a/src/app/qbs-setup-toolchains/watcomprobe.cpp b/src/app/qbs-setup-toolchains/watcomprobe.cpp index 13cb8d50c..cc9fee2b4 100644 --- a/src/app/qbs-setup-toolchains/watcomprobe.cpp +++ b/src/app/qbs-setup-toolchains/watcomprobe.cpp @@ -118,8 +118,8 @@ static QStringList dumpOutput(const QFileInfo &compiler, QStringView flag, p.start(compiler.absoluteFilePath(), args); p.waitForFinished(3000); fakeIn.remove(); - const QStringList lines = QString::fromUtf8(p.readAllStandardOutput()) - .split(QRegularExpression(QLatin1String("\\r?\\n"))); + QStringList lines = QString::fromUtf8(p.readAllStandardOutput()) + .split(QRegularExpression(QLatin1String("\\r?\\n"))); return lines; } diff --git a/src/app/qbs-setup-toolchains/xcodeprobe.cpp b/src/app/qbs-setup-toolchains/xcodeprobe.cpp index bb246742f..ea27e4143 100644 --- a/src/app/qbs-setup-toolchains/xcodeprobe.cpp +++ b/src/app/qbs-setup-toolchains/xcodeprobe.cpp @@ -150,7 +150,7 @@ void XcodeProbe::detectDeveloperPaths() if (!selectedXcode.waitForFinished(-1) || selectedXcode.exitCode()) { qbsInfo() << Tr::tr("Could not detect selected Xcode with /usr/bin/xcode-select"); } else { - QString path = QString::fromLocal8Bit(selectedXcode.readAllStandardOutput()); + QString path = QString::fromLocal8Bit(selectedXcode.readAllStandardOutput().trimmed()); addDeveloperPath(path); } addDeveloperPath(defaultDeveloperPath); @@ -163,7 +163,7 @@ void XcodeProbe::detectDeveloperPaths() qbsInfo() << Tr::tr("Could not detect additional Xcode installations with /usr/bin/mdfind"); } else { const auto paths = QString::fromLocal8Bit(launchServices.readAllStandardOutput()) - .split(QLatin1Char('\n'), QBS_SKIP_EMPTY_PARTS); + .split(QLatin1Char('\n'), Qt::SkipEmptyParts); for (const QString &path : paths) addDeveloperPath(path + QStringLiteral("/Contents/Developer")); } @@ -188,7 +188,7 @@ void XcodeProbe::setupDefaultToolchains(const QString &devPath, const QString &x << QStringLiteral("appletvsimulator") << QStringLiteral("watchos") << QStringLiteral("watchsimulator"); - for (const QString &platform : qAsConst(platforms)) { + for (const QString &platform : std::as_const(platforms)) { Profile platformProfile(xcodeName + QLatin1Char('-') + platform, settings); platformProfile.removeProfile(); platformProfile.setBaseProfile(installationProfile.name()); @@ -212,7 +212,7 @@ void XcodeProbe::detectAll() { int i = 1; detectDeveloperPaths(); - for (const QString &developerPath : qAsConst(developerPaths)) { + for (const QString &developerPath : std::as_const(developerPaths)) { QString profileName = QStringLiteral("xcode"); if (developerPath != defaultDeveloperPath) { const auto devPath = developerPath.toStdString(); diff --git a/src/app/qbs/CMakeLists.txt b/src/app/qbs/CMakeLists.txt index c190efccc..cd9240f98 100644 --- a/src/app/qbs/CMakeLists.txt +++ b/src/app/qbs/CMakeLists.txt @@ -7,6 +7,8 @@ set(SOURCES consoleprogressobserver.h ctrlchandler.cpp ctrlchandler.h + lspserver.cpp + lspserver.h main.cpp qbstool.cpp qbstool.h @@ -43,7 +45,11 @@ add_qbs_app(qbs "QBS_RELATIVE_LIBEXEC_PATH=\"${QBS_RELATIVE_LIBEXEC_PATH}\"" "QBS_RELATIVE_SEARCH_PATH=\"${QBS_RELATIVE_SEARCH_PATH}\"" "QBS_RELATIVE_PLUGINS_PATH=\"${QBS_RELATIVE_PLUGINS_PATH}\"" - DEPENDS qbscore qbsconsolelogger + DEPENDS + qbscore + qbsconsolelogger + qtclsp + Qt${QT_VERSION_MAJOR}::Network SOURCES ${SOURCES} ${PARSER_SOURCES} ) diff --git a/src/app/qbs/commandlinefrontend.cpp b/src/app/qbs/commandlinefrontend.cpp index 489be4ed5..200740145 100644 --- a/src/app/qbs/commandlinefrontend.cpp +++ b/src/app/qbs/commandlinefrontend.cpp @@ -97,9 +97,9 @@ void CommandLineFrontend::checkCancelStatus() m_cancelTimer->stop(); if (m_resolveJobs.empty() && m_buildJobs.empty()) std::exit(EXIT_FAILURE); - for (AbstractJob * const job : qAsConst(m_resolveJobs)) + for (AbstractJob * const job : std::as_const(m_resolveJobs)) job->cancel(); - for (AbstractJob * const job : qAsConst(m_buildJobs)) + for (AbstractJob * const job : std::as_const(m_buildJobs)) job->cancel(); break; case CancelStatusCanceling: @@ -152,11 +152,11 @@ void CommandLineFrontend::start() params.setDryRun(m_parser.dryRun()); params.setForceProbeExecution(m_parser.forceProbesExecution()); params.setWaitLockBuildGraph(m_parser.waitLockBuildGraph()); - params.setFallbackProviderEnabled(!m_parser.disableFallbackProvider()); params.setLogElapsedTime(m_parser.logTime()); params.setSettingsDirectory(m_settings->baseDirectory()); params.setOverrideBuildGraphData(m_parser.command() == ResolveCommandType); params.setPropertyCheckingMode(ErrorHandlingMode::Strict); + params.setDeprecationWarningMode(m_parser.deprecationWarningMode()); if (!m_parser.buildBeforeInstalling() || !m_parser.commandCanResolve()) params.setRestoreBehavior(SetupProjectParameters::RestoreOnly); const auto buildConfigs = m_parser.buildConfigurations(); @@ -183,6 +183,7 @@ void CommandLineFrontend::start() params.setConfigurationName(configurationName); params.setBuildRoot(buildDirectory(profileName)); params.setOverriddenValues(userConfig); + params.setMaxJobCount(m_parser.jobCount(profileName)); SetupProjectJob * const job = Project().setupProject(params, ConsoleLogger::instance().logSink(), this); connectJob(job); @@ -348,12 +349,12 @@ CommandLineFrontend::ProductMap CommandLineFrontend::productsToUse() const ProductMap products; QStringList productNames; const bool useAll = m_parser.products().empty(); - for (const Project &project : qAsConst(m_projects)) { + for (const Project &project : std::as_const(m_projects)) { QList<ProductData> &productList = products[project]; const ProjectData projectData = project.projectData(); for (const ProductData &product : projectData.allProducts()) { - productNames << product.name(); - if (useAll || m_parser.products().contains(product.name())) { + productNames << product.fullDisplayName(); + if (useAll || m_parser.products().contains(product.fullDisplayName())) { productList.push_back(product); } } @@ -431,7 +432,7 @@ void CommandLineFrontend::handleProjectsResolved() void CommandLineFrontend::makeClean() { if (m_parser.products().empty()) { - for (const Project &project : qAsConst(m_projects)) { + for (const Project &project : std::as_const(m_projects)) { m_buildJobs << project.cleanAllProducts(m_parser.cleanOptions(project.profile()), this); } } else { @@ -489,9 +490,22 @@ QString CommandLineFrontend::buildDirectory(const QString &profileName) const } QString projectName(QFileInfo(m_parser.projectFilePath()).baseName()); + QString originalBuildDir = buildDir; buildDir.replace(BuildDirectoryOption::magicProjectString(), projectName); + const QString buildDirPlaceHolderMsgTemplate = Tr::tr( + "You must provide the path to the project file when using build directory " + "placeholder '%1'."); + if (buildDir != originalBuildDir && projectName.isEmpty()) { + throw ErrorInfo( + buildDirPlaceHolderMsgTemplate.arg(BuildDirectoryOption::magicProjectString())); + } QString projectDir(QFileInfo(m_parser.projectFilePath()).path()); + originalBuildDir = buildDir; buildDir.replace(BuildDirectoryOption::magicProjectDirString(), projectDir); + if (buildDir != originalBuildDir && projectDir.isEmpty()) { + throw ErrorInfo( + buildDirPlaceHolderMsgTemplate.arg(BuildDirectoryOption::magicProjectDirString())); + } if (!QFileInfo(buildDir).isAbsolute()) buildDir = QDir::currentPath() + QLatin1Char('/') + buildDir; buildDir = QDir::cleanPath(buildDir); @@ -503,7 +517,7 @@ void CommandLineFrontend::build() if (m_parser.products().empty()) { const Project::ProductSelection productSelection = m_parser.withNonDefaultProducts() ? Project::ProductSelectionWithNonDefault : Project::ProductSelectionDefaultOnly; - for (const Project &project : qAsConst(m_projects)) + for (const Project &project : std::as_const(m_projects)) m_buildJobs << project.buildAllProducts(buildOptions(project), productSelection, this); } else { const ProductMap &products = productsToUse(); @@ -564,7 +578,7 @@ int CommandLineFrontend::runTarget() const QString executableFilePath = productToRun.targetExecutable(); if (executableFilePath.isEmpty()) { throw ErrorInfo(Tr::tr("Cannot run: Product '%1' is not an application.") - .arg(productToRun.name())); + .arg(productToRun.fullDisplayName())); } RunEnvironment runEnvironment = m_projects.front().getRunEnvironment(productToRun, m_parser.installOptions(m_projects.front().profile()), @@ -609,7 +623,7 @@ void CommandLineFrontend::listProducts() void CommandLineFrontend::connectBuildJobs() { - for (AbstractJob * const job : qAsConst(m_buildJobs)) + for (AbstractJob * const job : std::as_const(m_buildJobs)) connectBuildJob(job); } @@ -647,7 +661,7 @@ ProductData CommandLineFrontend::getTheOneRunnableProduct() if (m_parser.products().size() == 1) { for (const ProductData &p : m_projects.front().projectData().allProducts()) { - if (p.name() == m_parser.products().constFirst()) + if (p.fullDisplayName() == m_parser.products().constFirst()) return p; } QBS_CHECK(false); @@ -671,14 +685,8 @@ ProductData CommandLineFrontend::getTheOneRunnableProduct() ErrorInfo error(Tr::tr("Ambiguous use of command '%1': No product given, but project " "has more than one runnable product.").arg(m_parser.commandName())); error.append(Tr::tr("Use the '--products' option with one of the following products:")); - for (const ProductData &p : qAsConst(runnableProducts)) { - QString productRepr = QLatin1String("\t") + p.name(); - if (p.profile() != m_projects.front().profile()) { - productRepr.append(QLatin1String(" [")).append(p.profile()) - .append(QLatin1Char(']')); - } - error.append(productRepr); - } + for (const ProductData &p : std::as_const(runnableProducts)) + error.append(QLatin1String("\t") + p.fullDisplayName()); throw error; } diff --git a/src/app/qbs/lspserver.cpp b/src/app/qbs/lspserver.cpp new file mode 100644 index 000000000..c6cce6706 --- /dev/null +++ b/src/app/qbs/lspserver.cpp @@ -0,0 +1,629 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "lspserver.h" + +#include <api/projectdata.h> +#include <logging/translator.h> +#include <lsp/basemessage.h> +#include <lsp/completion.h> +#include <lsp/initializemessages.h> +#include <lsp/jsonrpcmessages.h> +#include <lsp/messages.h> +#include <lsp/servercapabilities.h> +#include <lsp/textsynchronization.h> +#include <parser/qmljsastvisitor_p.h> +#include <parser/qmljslexer_p.h> +#include <parser/qmljsparser_p.h> +#include <tools/qbsassert.h> +#include <tools/stlutils.h> + +#include <QBuffer> +#include <QLocalServer> +#include <QLocalSocket> +#include <QMap> + +#include <unordered_map> +#ifdef Q_OS_WINDOWS +#include <process.h> +#else +#include <unistd.h> +#endif + +namespace qbs::Internal { + +static int getPid() +{ +#ifdef Q_OS_WINDOWS + return _getpid(); +#else + return getpid(); +#endif +} + +using LspErrorResponse = lsp::ResponseError<std::nullptr_t>; +using LspErrorCode = LspErrorResponse::ErrorCodes; + +class Document { +public: + bool isPositionUpToDate(const CodePosition &pos) const; + bool isPositionUpToDate(const lsp::Position &pos) const; + + QString savedContent; + QString currentContent; +}; + +static CodePosition posFromLspPos(const lsp::Position &pos) +{ + return {pos.line() + 1, pos.character() + 1}; +} + +static lsp::Position lspPosFromCodeLocation(const CodeLocation &loc) +{ + return {loc.line() - 1, loc.column() - 1}; +} + +static QString uriToString(const lsp::DocumentUri &uri) +{ + return uri.toFilePath([](const lsp::Utils::FilePath &fp) { return fp; }); +} + +static int posToOffset(const CodePosition &pos, const QString &doc); +static int posToOffset(const lsp::Position &pos, const QString &doc) +{ + return posToOffset(posFromLspPos(pos), doc); +} + +class AstNodeLocator : public QbsQmlJS::AST::Visitor +{ +public: + AstNodeLocator(int position, QbsQmlJS::AST::UiProgram &ast) + : m_position(position) + { + ast.accept(this); + } + + QList<QbsQmlJS::AST::Node *> path() const { return m_path; } + +private: + bool preVisit(QbsQmlJS::AST::Node *node) override + { + if (int(node->firstSourceLocation().offset) > m_position) + return false; + if (int(node->lastSourceLocation().offset) < m_position) + return false; + m_path << node; + return true; + } + + const int m_position; + QList<QbsQmlJS::AST::Node *> m_path; +}; + +class LspServer::Private +{ +public: + void setupConnection(); + void handleIncomingData(); + void discardSocket(); + template<typename T> void sendResponse(const T &msg); + void sendErrorResponse(LspErrorCode code, const QString &message); + void sendErrorNotification(const QString &message); + void sendNoSuchDocumentError(const QString &filePath); + void sendMessage(const lsp::JsonObject &msg); + void sendMessage(const lsp::JsonRpcMessage &msg); + void handleCurrentMessage(); + void handleInitializeRequest(); + void handleInitializedNotification(); + void handleGotoDefinitionRequest(); + void handleCompletionRequest(); + void handleShutdownRequest(); + void handleDidOpenNotification(); + void handleDidChangeNotification(); + void handleDidSaveNotification(); + void handleDidCloseNotification(); + + QLocalServer server; + QBuffer incomingData; + lsp::BaseMessage currentMessage; + QJsonObject messageObject; + QLocalSocket *socket = nullptr; + ProjectData projectData; + CodeLinks codeLinks; + std::unordered_map<QString, Document> documents; + + enum class State { None, InitRequest, InitNotification, Shutdown }; + State state = State::None; +}; + +LspServer::LspServer() : d(new Private) +{ + if (!d->server.listen(QString::fromLatin1("qbs-lsp-%1").arg(getPid()))) { + // This is not fatal, qbs main operation can continue. + qWarning() << "failed to open lsp socket:" << d->server.errorString(); + return; + } + QObject::connect(&d->server, &QLocalServer::newConnection, [this] { + d->socket = d->server.nextPendingConnection(); + d->setupConnection(); + d->server.close(); + }); +} + +LspServer::~LspServer() { delete d; } + +void LspServer::updateProjectData(const ProjectData &projectData, const CodeLinks &codeLinks) +{ + d->projectData = projectData; + d->codeLinks = codeLinks; +} + +QString LspServer::socketPath() const { return d->server.fullServerName(); } + +void LspServer::Private::setupConnection() +{ + QBS_ASSERT(socket, return); + + QObject::connect(socket, &QLocalSocket::errorOccurred, [this] { discardSocket(); }); + QObject::connect(socket, &QLocalSocket::disconnected, [this] { discardSocket(); }); + QObject::connect(socket, &QLocalSocket::readyRead, [this] { handleIncomingData(); }); + incomingData.open(QIODevice::ReadWrite | QIODevice::Append); + handleIncomingData(); +} + +void LspServer::Private::handleIncomingData() +{ + const int pos = incomingData.pos(); + incomingData.write(socket->readAll()); + incomingData.seek(pos); + QString parseError; + lsp::BaseMessage::parse(&incomingData, parseError, currentMessage); + if (!parseError.isEmpty()) + return sendErrorResponse(LspErrorResponse::ParseError, parseError); + if (currentMessage.isComplete()) { + incomingData.buffer().remove(0, incomingData.pos()); + incomingData.seek(0); + handleCurrentMessage(); + currentMessage = {}; + messageObject = {}; + if (socket) + handleIncomingData(); + } +} + +void LspServer::Private::discardSocket() +{ + socket->disconnect(); + socket->deleteLater(); + socket = nullptr; +} + +template<typename T> void LspServer::Private::sendResponse(const T &msg) +{ + lsp::Response<T, std::nullptr_t> response(lsp::MessageId(messageObject.value(lsp::idKey))); + response.setResult(msg); + sendMessage(response); +} + +void LspServer::Private::sendErrorResponse(LspErrorCode code, const QString &message) +{ + lsp::Response<lsp::JsonObject, std::nullptr_t> response(lsp::MessageId( + messageObject.value(lsp::idKey))); + lsp::ResponseError<std::nullptr_t> error; + error.setCode(code); + error.setMessage(message); + response.setError(error); + sendMessage(response); +} + +void LspServer::Private::sendErrorNotification(const QString &message) +{ + lsp::ShowMessageParams params; + params.setType(lsp::Error); + params.setMessage(message); + sendMessage(lsp::ShowMessageNotification(params)); +} + +void LspServer::Private::sendNoSuchDocumentError(const QString &filePath) +{ + sendErrorNotification(Tr::tr("No such document: '%1'").arg(filePath)); +} + +void LspServer::Private::sendMessage(const lsp::JsonObject &msg) +{ + sendMessage(lsp::JsonRpcMessage(msg)); +} + +void LspServer::Private::sendMessage(const lsp::JsonRpcMessage &msg) +{ + lsp::BaseMessage baseMsg = msg.toBaseMessage(); + socket->write(baseMsg.header()); + socket->write(baseMsg.content); +} + +void LspServer::Private::handleCurrentMessage() +{ + messageObject = lsp::JsonRpcMessage(currentMessage).toJsonObject(); + const QString method = messageObject.value(lsp::methodKey).toString(); + if (method == QLatin1String("exit")) + return discardSocket(); + if (state == State::Shutdown) { + return sendErrorResponse(LspErrorResponse::InvalidRequest, + Tr::tr("Method '%1' not allowed after shutdown.")); + } + if (method == "shutdown") + return handleShutdownRequest(); + if (method == QLatin1String("initialize")) + return handleInitializeRequest(); + if (state == State::None) { + return sendErrorResponse(LspErrorResponse::ServerNotInitialized, + Tr::tr("First message must be initialize request.")); + } + if (method == "initialized") + return handleInitializedNotification(); + if (method == "textDocument/didOpen") + return handleDidOpenNotification(); + if (method == "textDocument/didChange") + return handleDidChangeNotification(); + if (method == "textDocument/didSave") + return handleDidSaveNotification(); + if (method == "textDocument/didClose") + return handleDidCloseNotification(); + if (method == "textDocument/definition") + return handleGotoDefinitionRequest(); + if (method == "textDocument/completion") + return handleCompletionRequest(); + + sendErrorResponse(LspErrorResponse::MethodNotFound, Tr::tr("This server can do very little.")); +} + +void LspServer::Private::handleInitializeRequest() +{ + if (state != State::None) { + return sendErrorResponse(LspErrorResponse::InvalidRequest, + Tr::tr("Received initialize request in initialized state.")); + } + state = State::InitRequest; + + lsp::ServerInfo serverInfo; + serverInfo.insert(lsp::nameKey, "qbs"); + serverInfo.insert(lsp::versionKey, QBS_VERSION); + lsp::InitializeResult result; + result.insert(lsp::serverInfoKey, serverInfo); + lsp::ServerCapabilities capabilities; // TODO: hover + capabilities.setDefinitionProvider(true); + capabilities.setTextDocumentSync({int(lsp::TextDocumentSyncKind::Incremental)}); + lsp::ServerCapabilities::CompletionOptions completionOptions; + completionOptions.setTriggerCharacters({"."}); + capabilities.setCompletionProvider(completionOptions); + result.setCapabilities(capabilities); + sendResponse(result); +} + +void LspServer::Private::handleInitializedNotification() +{ + if (state != State::InitRequest) { + return sendErrorResponse(LspErrorResponse::InvalidRequest, + Tr::tr("Unexpected initialized notification.")); + } + state = State::InitNotification; +} + +void LspServer::Private::handleGotoDefinitionRequest() +{ + const lsp::TextDocumentPositionParams params(messageObject.value(lsp::paramsKey)); + const QString sourceFile = params.textDocument().uri().toLocalFile(); + const Document *sourceDoc = nullptr; + if (const auto it = documents.find(sourceFile); it != documents.end()) + sourceDoc = &it->second; + const auto fileEntry = codeLinks.constFind(sourceFile); + if (fileEntry == codeLinks.constEnd()) + return sendResponse(nullptr); + const CodePosition sourcePos = posFromLspPos(params.position()); + if (sourceDoc && !sourceDoc->isPositionUpToDate(sourcePos)) + return sendResponse(nullptr); + for (auto it = fileEntry->cbegin(); it != fileEntry->cend(); ++it) { + if (!it.key().contains(sourcePos)) + continue; + if (sourceDoc && !sourceDoc->isPositionUpToDate(it.key().end())) + return sendResponse(nullptr); + QList<CodeLocation> targets = it.value(); + QBS_ASSERT(!targets.isEmpty(), return sendResponse(nullptr)); + for (auto it = targets.begin(); it != targets.end();) { + const Document *targetDoc = nullptr; + if (it->filePath() == sourceFile) + targetDoc = sourceDoc; + else if (const auto docIt = documents.find(it->filePath()); docIt != documents.end()) + targetDoc = &docIt->second; + if (targetDoc && !targetDoc->isPositionUpToDate(CodePosition(it->line(), it->column()))) + it = targets.erase(it); + else + ++it; + } + struct JsonArray : public QJsonArray { void reserve(std::size_t) {}}; + const auto locations = transformed<JsonArray>(targets, + [](const CodeLocation &loc) { + const lsp::Position startPos = lspPosFromCodeLocation(loc); + const lsp::Position endPos(startPos.line(), startPos.character() + 1); + lsp::Location targetLocation; + targetLocation.setUri(lsp::DocumentUri::fromProtocol( + QUrl::fromLocalFile(loc.filePath()).toString())); + targetLocation.setRange({startPos, endPos}); + return QJsonObject(targetLocation); + }); + if (locations.size() == 1) + return sendResponse(locations.first().toObject()); + return sendResponse(locations); + } + sendResponse(nullptr); +} + +// We operate under the assumption that the client has basic QML support. +// Therefore, we only provide completion for qbs modules and their properties. +// Only a simple prefix match is implemented, with no regard to the contents after the cursor. +void LspServer::Private::handleCompletionRequest() +{ + if (!projectData.isValid()) + return sendResponse(nullptr); + + const lsp::CompletionParams params(messageObject.value(lsp::paramsKey)); + const QString sourceFile = params.textDocument().uri().toLocalFile(); + const Document *sourceDoc = nullptr; + if (const auto it = documents.find(sourceFile); it != documents.end()) + sourceDoc = &it->second; + if (!sourceDoc) + return sendResponse(nullptr); + + // If there are products corresponding to this file, check only these when looking for modules. + // Otherwise, check all products. + const QList<ProductData> allProducts = projectData.allProducts(); + if (allProducts.isEmpty()) + return sendResponse(nullptr); + QList<ProductData> relevantProducts; + for (const ProductData &p : allProducts) { + if (p.location().filePath() == sourceFile) + relevantProducts << p; + } + if (relevantProducts.isEmpty()) + relevantProducts = allProducts; + + QString identifierPrefix; + QStringList modulePrefixes; + const int offset = posToOffset(params.position(), sourceDoc->currentContent) - 1; + if (offset < 0 || offset >= sourceDoc->currentContent.length()) + return sendResponse(nullptr); + const auto collectFromRawString = [&] { + int currentPos = offset; + const auto constructIdentifier = [&] { + QString id; + while (currentPos >= 0) { + const QChar c = sourceDoc->currentContent.at(currentPos); + if (!c.isLetterOrNumber() && c != '_') + break; + id.prepend(c); + --currentPos; + } + return id; + }; + identifierPrefix = constructIdentifier(); + while (true) { + if (currentPos <= 0 || sourceDoc->currentContent.at(currentPos) != '.') + return; + --currentPos; + const QString modulePrefix = constructIdentifier(); + if (modulePrefix.isEmpty()) + return; + modulePrefixes.prepend(modulePrefix); + } + }; + + // Parse the current file. Note that completion usually happens on invalid code, which + // often confuses the parser so much that it yields unusable results. Therefore, we always + // gather our input parameters from the raw string. We only use the parse result to skip + // completion in contexts where it is undesirable. + QbsQmlJS::Engine engine; + QbsQmlJS::Lexer lexer(&engine); + lexer.setCode(sourceDoc->currentContent, 1); + QbsQmlJS::Parser parser(&engine); + parser.parse(); + if (parser.ast()) { + AstNodeLocator locator(offset, *parser.ast()); + const QList<QbsQmlJS::AST::Node *> &astPath = locator.path(); + if (!astPath.isEmpty()) { + switch (astPath.last()->kind) { + case QbsQmlJS::AST::Node::Kind_FieldMemberExpression: + case QbsQmlJS::AST::Node::Kind_UiObjectDefinition: + case QbsQmlJS::AST::Node::Kind_UiQualifiedId: + case QbsQmlJS::AST::Node::Kind_UiScriptBinding: + break; + default: + return sendResponse(nullptr); + } + } + } + + collectFromRawString(); + if (modulePrefixes.isEmpty() && identifierPrefix.isEmpty()) + return sendResponse(nullptr); // We do not want to start completion from nothing. + + QJsonArray results; + QMap<QString, QString> namesAndTypes; + for (const ProductData &product : std::as_const(relevantProducts)) { + const PropertyMap &moduleProps = product.moduleProperties(); + const QStringList allModules = moduleProps.allModules(); + const QString moduleNameOrPrefix = modulePrefixes.join('.'); + + // Case 1: Prefixes match a module name. Identifier can only expand to the name + // of a module property. + // Example: "Qt.core.a^" -> "Qt.core.availableBuildVariants" + if (!modulePrefixes.isEmpty() && allModules.contains(moduleNameOrPrefix)) { + for (const PropertyMap::PropertyInfo &info : + moduleProps.allPropertiesForModule(moduleNameOrPrefix)) { + if (info.isBuiltin) + continue; + if (!identifierPrefix.isEmpty() && !info.name.startsWith(identifierPrefix)) + continue; + namesAndTypes.insert(info.name, info.type); + } + continue; + } + + // Case 2: Isolated identifier. Can only expand to a module name. + // Example: "Q^" -> "Qt.core", "Qt.widgets", ... + // Case 3: Prefixes match a module prefix. Identifier can only expand to a module name. + // Example: "Qt.c^" -> "Qt.core", "Qt.concurrent", ... + QString fullPrefix = identifierPrefix; + int nameOffset = 0; + if (!modulePrefixes.isEmpty()) { + fullPrefix.prepend(moduleNameOrPrefix + '.'); + nameOffset = moduleNameOrPrefix.length() + 1; + } + for (const QString &module : allModules) { + if (module.startsWith(fullPrefix)) + namesAndTypes.insert(module.mid(nameOffset), {}); + } + } + + for (auto it = namesAndTypes.cbegin(); it != namesAndTypes.cend(); ++it) { + lsp::CompletionItem item; + item.setLabel(it.key()); + if (!it.value().isEmpty()) + item.setDetail(it.value()); + results.append(QJsonObject(item)); + }; + sendResponse(results); +} + +void LspServer::Private::handleShutdownRequest() +{ + state = State::Shutdown; + sendResponse(nullptr); +} + +void LspServer::Private::handleDidOpenNotification() +{ + const lsp::TextDocumentItem docItem = lsp::DidOpenTextDocumentNotification(messageObject) + .params().value_or(lsp::DidOpenTextDocumentParams()) + .textDocument(); + if (!docItem.isValid()) + return sendErrorNotification(Tr::tr("Invalid didOpen parameters.")); + const QString filePath = uriToString(docItem.uri()); + Document &doc = documents[filePath]; + doc.savedContent = doc.currentContent = docItem.text(); +} + +void LspServer::Private::handleDidChangeNotification() +{ + const auto params = lsp::DidChangeTextDocumentNotification(messageObject) + .params().value_or(lsp::DidChangeTextDocumentParams()); + if (!params.isValid()) + return sendErrorNotification(Tr::tr("Invalid didChange parameters.")); + const QString filePath = uriToString(params.textDocument().uri()); + const auto docIt = documents.find(filePath); + if (docIt == documents.end()) + return sendNoSuchDocumentError(filePath); + Document &doc = docIt->second; + const auto changes = params.contentChanges(); + for (const auto &change : changes) { + const auto range = change.range(); + if (!range) { + doc.currentContent = change.text(); + continue; + } + const int startPos = posToOffset(range->start(), doc.currentContent); + const int endPos = posToOffset(range->end(), doc.currentContent); + if (startPos == -1 || endPos == -1 || startPos > endPos) + return sendErrorResponse(LspErrorResponse::InvalidParams, Tr::tr("Invalid range.")); + doc.currentContent.replace(startPos, endPos - startPos, change.text()); + } +} + +void LspServer::Private::handleDidSaveNotification() +{ + const QString filePath = uriToString(lsp::DidSaveTextDocumentNotification(messageObject) + .params().value_or(lsp::DidSaveTextDocumentParams()) + .textDocument().uri()); + const auto docIt = documents.find(filePath); + if (docIt == documents.end()) + return sendNoSuchDocumentError(filePath); + docIt->second.savedContent = docIt->second.currentContent; + + // TODO: Mark the project data as out of date until the next update(),if the document + // is in buildSystemFiles(). +} + +void LspServer::Private::handleDidCloseNotification() +{ + const QString filePath = uriToString(lsp::DidCloseTextDocumentNotification(messageObject) + .params().value_or(lsp::DidCloseTextDocumentParams()) + .textDocument().uri()); + const auto docIt = documents.find(filePath); + if (docIt == documents.end()) + return sendNoSuchDocumentError(filePath); + documents.erase(docIt); +} + +static int posToOffset(const CodePosition &pos, const QString &doc) +{ + int offset = 0; + for (int newlines = 0, next = 0; newlines < pos.line() - 1; ++newlines) { + offset = doc.indexOf('\n', next); + if (offset == -1) + return -1; + next = offset + 1; + } + return offset + pos.column(); +} + +bool Document::isPositionUpToDate(const CodePosition &pos) const +{ + const int origOffset = posToOffset(pos, savedContent); + if (origOffset > int(currentContent.size())) + return false; + return QStringView(currentContent).left(origOffset) + == QStringView(savedContent).left(origOffset); +} + +bool Document::isPositionUpToDate(const lsp::Position &pos) const +{ + return isPositionUpToDate(posFromLspPos(pos)); +} + +} // namespace qbs::Internal + diff --git a/src/app/qbs/lspserver.h b/src/app/qbs/lspserver.h new file mode 100644 index 000000000..566808309 --- /dev/null +++ b/src/app/qbs/lspserver.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once + +#include <tools/codelocation.h> + +#include <QString> + +namespace qbs { +class ProjectData; +namespace Internal { + +class LspServer +{ +public: + LspServer(); + ~LspServer(); + + void updateProjectData(const ProjectData &projectData, const CodeLinks &codeLinks); + QString socketPath() const; + +private: + class Private; + Private * const d; +}; + +} // namespace Internal +} // namespace qbs diff --git a/src/app/qbs/parser/commandlineoption.cpp b/src/app/qbs/parser/commandlineoption.cpp index a09f36c2c..7572b4e66 100644 --- a/src/app/qbs/parser/commandlineoption.cpp +++ b/src/app/qbs/parser/commandlineoption.cpp @@ -334,7 +334,7 @@ void StringListOption::doParse(const QString &representation, QStringList &input throw ErrorInfo(Tr::tr("Invalid use of option '%1': Argument list must not be empty.\n" "Usage: %2").arg(representation, description(command()))); } - for (const QString &element : qAsConst(m_arguments)) { + for (const QString &element : std::as_const(m_arguments)) { if (element.isEmpty()) { throw ErrorInfo(Tr::tr("Invalid use of option '%1': Argument list must not contain " "empty elements.\nUsage: %2") @@ -663,27 +663,51 @@ void CommandEchoModeOption::doParse(const QString &representation, QStringList & m_echoMode = commandEchoModeFromName(mode); } -QString WaitLockOption::description(CommandType command) const +QString DeprecationWarningsOption::description(CommandType command) const { Q_UNUSED(command); - return Tr::tr("%1\n\tWait indefinitely for other processes to release the build graph lock.\n") - .arg(longRepresentation()); + return Tr::tr("%1 <mode>\n" + "\tWhat to do when encountering deprecated items or properties.\n" + "\tPossible values are '%2'.\n" + "\tThe default is '%3'.\n") + .arg(longRepresentation(), + allDeprecationWarningModeStrings().join(QLatin1String("', '")), + deprecationWarningModeName(defaultDeprecationWarningMode())); } -QString WaitLockOption::longRepresentation() const +QString DeprecationWarningsOption::longRepresentation() const { - return QStringLiteral("--wait-lock"); + return QStringLiteral("--deprecation-warnings"); } -QString DisableFallbackProviderOption::description(CommandType) const +void DeprecationWarningsOption::doParse(const QString &representation, QStringList &input) { - return Tr::tr("%1\n\tDo not fall back to pkg-config if a dependency is not found.\n") + const QString mode = getArgument(representation, input); + if (mode.isEmpty()) { + throw ErrorInfo(Tr::tr("Invalid use of option '%1': No deprecation warning mode given.\n" + "Usage: %2") + .arg(representation, description(command()))); + } + + if (!allDeprecationWarningModeStrings().contains(mode)) { + throw ErrorInfo(Tr::tr("Invalid use of option '%1': " + "Invalid deprecation warning mode '%2' given.\nUsage: %3") + .arg(representation, mode, description(command()))); + } + + m_mode = deprecationWarningModeFromName(mode); +} + +QString WaitLockOption::description(CommandType command) const +{ + Q_UNUSED(command); + return Tr::tr("%1\n\tWait indefinitely for other processes to release the build graph lock.\n") .arg(longRepresentation()); } -QString DisableFallbackProviderOption::longRepresentation() const +QString WaitLockOption::longRepresentation() const { - return QStringLiteral("--no-fallback-module-provider"); + return QStringLiteral("--wait-lock"); } QString RunEnvConfigOption::description(CommandType command) const diff --git a/src/app/qbs/parser/commandlineoption.h b/src/app/qbs/parser/commandlineoption.h index f8ec1c735..7b8baeae2 100644 --- a/src/app/qbs/parser/commandlineoption.h +++ b/src/app/qbs/parser/commandlineoption.h @@ -42,6 +42,7 @@ #include "commandtype.h" #include <tools/commandechomode.h> +#include <tools/deprecationwarningmode.h> #include <tools/joblimits.h> #include <QtCore/qstringlist.h> @@ -54,7 +55,9 @@ public: enum Type { FileOptionType, BuildDirectoryOptionType, - LogLevelOptionType, VerboseOptionType, QuietOptionType, + LogLevelOptionType, + VerboseOptionType, + QuietOptionType, JobsOptionType, KeepGoingOptionType, DryRunOptionType, @@ -63,7 +66,9 @@ public: ChangedFilesOptionType, ProductsOptionType, NoInstallOptionType, - InstallRootOptionType, RemoveFirstOptionType, NoBuildOptionType, + InstallRootOptionType, + RemoveFirstOptionType, + NoBuildOptionType, ForceTimestampCheckOptionType, ForceOutputCheckOptionType, BuildNonDefaultOptionType, @@ -75,7 +80,7 @@ public: GeneratorOptionType, WaitLockOptionType, RunEnvConfigOptionType, - DisableFallbackProviderType, + DeprecationWarningsOptionType, }; virtual ~CommandLineOption(); @@ -367,6 +372,20 @@ private: CommandEchoMode m_echoMode = CommandEchoModeInvalid; }; +class DeprecationWarningsOption : public CommandLineOption +{ +public: + QString description(CommandType command) const override; + QString shortRepresentation() const override { return {}; } + QString longRepresentation() const override; + DeprecationWarningMode mode() const { return m_mode; } + +private: + void doParse(const QString &representation, QStringList &input) override; + + DeprecationWarningMode m_mode = defaultDeprecationWarningMode(); +}; + class SettingsDirOption : public CommandLineOption { public: @@ -415,14 +434,6 @@ public: QString longRepresentation() const override; }; -class DisableFallbackProviderOption : public OnOffOption -{ -public: - QString description(CommandType command) const override; - QString shortRepresentation() const override { return {}; } - QString longRepresentation() const override; -}; - } // namespace qbs #endif // QBS_COMMANDLINEOPTION_H diff --git a/src/app/qbs/parser/commandlineoptionpool.cpp b/src/app/qbs/parser/commandlineoptionpool.cpp index 63711f623..692c9c737 100644 --- a/src/app/qbs/parser/commandlineoptionpool.cpp +++ b/src/app/qbs/parser/commandlineoptionpool.cpp @@ -128,12 +128,12 @@ CommandLineOption *CommandLineOptionPool::getOption(CommandLineOption::Type type case CommandLineOption::WaitLockOptionType: option = new WaitLockOption; break; - case CommandLineOption::DisableFallbackProviderType: - option = new DisableFallbackProviderOption; - break; case CommandLineOption::RunEnvConfigOptionType: option = new RunEnvConfigOption; break; + case CommandLineOption::DeprecationWarningsOptionType: + option = new DeprecationWarningsOption; + break; default: qFatal("Unknown option type %d", type); } @@ -276,15 +276,15 @@ WaitLockOption *CommandLineOptionPool::waitLockOption() const return static_cast<WaitLockOption *>(getOption(CommandLineOption::WaitLockOptionType)); } -DisableFallbackProviderOption *CommandLineOptionPool::disableFallbackProviderOption() const +RunEnvConfigOption *CommandLineOptionPool::runEnvConfigOption() const { - return static_cast<DisableFallbackProviderOption *>( - getOption(CommandLineOption::DisableFallbackProviderType)); + return static_cast<RunEnvConfigOption *>(getOption(CommandLineOption::RunEnvConfigOptionType)); } -RunEnvConfigOption *CommandLineOptionPool::runEnvConfigOption() const +DeprecationWarningsOption *CommandLineOptionPool::deprecationWarningsOption() const { - return static_cast<RunEnvConfigOption *>(getOption(CommandLineOption::RunEnvConfigOptionType)); + return static_cast<DeprecationWarningsOption *> + (getOption(CommandLineOption::DeprecationWarningsOptionType)); } } // namespace qbs diff --git a/src/app/qbs/parser/commandlineoptionpool.h b/src/app/qbs/parser/commandlineoptionpool.h index c7ac263e1..022e9fd09 100644 --- a/src/app/qbs/parser/commandlineoptionpool.h +++ b/src/app/qbs/parser/commandlineoptionpool.h @@ -77,8 +77,8 @@ public: RespectProjectJobLimitsOption *respectProjectJobLimitsOption() const; GeneratorOption *generatorOption() const; WaitLockOption *waitLockOption() const; - DisableFallbackProviderOption *disableFallbackProviderOption() const; RunEnvConfigOption *runEnvConfigOption() const; + DeprecationWarningsOption *deprecationWarningsOption() const; private: mutable QHash<CommandLineOption::Type, CommandLineOption *> m_options; diff --git a/src/app/qbs/parser/commandlineparser.cpp b/src/app/qbs/parser/commandlineparser.cpp index 0f70b3fe9..c548cf2b5 100644 --- a/src/app/qbs/parser/commandlineparser.cpp +++ b/src/app/qbs/parser/commandlineparser.cpp @@ -65,6 +65,7 @@ #include <QtCore/qmap.h> #include <QtCore/qtextstream.h> +#include <algorithm> #include <utility> #ifdef Q_OS_UNIX @@ -155,17 +156,11 @@ QString CommandLineParser::projectBuildDirectory() const BuildOptions CommandLineParser::buildOptions(const QString &profile) const { - Settings settings(settingsDir()); - Preferences preferences(&settings, profile); - - if (d->buildOptions.maxJobCount() <= 0) { - d->buildOptions.setMaxJobCount(preferences.jobs()); - } - + d->buildOptions.setMaxJobCount(jobCount(profile)); if (d->buildOptions.echoMode() < 0) { - d->buildOptions.setEchoMode(preferences.defaultEchoMode()); + Settings settings(settingsDir()); + d->buildOptions.setEchoMode(Preferences(&settings, profile).defaultEchoMode()); } - return d->buildOptions; } @@ -202,6 +197,15 @@ InstallOptions CommandLineParser::installOptions(const QString &profile) const return options; } +int CommandLineParser::jobCount(const QString &profile) const +{ + if (const int explicitJobCount = d->optionPool.jobsOption()->jobCount(); explicitJobCount > 0) + return explicitJobCount; + + Settings settings(settingsDir()); + return Preferences(&settings, profile).jobs(); +} + bool CommandLineParser::forceTimestampCheck() const { return d->optionPool.forceTimestampCheckOption()->enabled(); @@ -227,11 +231,6 @@ bool CommandLineParser::waitLockBuildGraph() const return d->optionPool.waitLockOption()->enabled(); } -bool CommandLineParser::disableFallbackProvider() const -{ - return d->optionPool.disableFallbackProviderOption()->enabled(); -} - bool CommandLineParser::logTime() const { return d->logTime; @@ -278,6 +277,11 @@ QString CommandLineParser::settingsDir() const return d->settingsDir(); } +DeprecationWarningMode CommandLineParser::deprecationWarningMode() const +{ + return d->optionPool.deprecationWarningsOption()->mode(); +} + QString CommandLineParser::commandName() const { return d->command->representation(); @@ -329,7 +333,19 @@ void CommandLineParser::CommandLineParserPrivate::doParse() } else { command = commandFromString(commandLine.front()); if (command) { - commandLine.removeFirst(); + const QString commandName = commandLine.takeFirst(); + + // if the command line contains a `<command>` with + // either `-h` or `--help` switch, we transform + // it to corresponding `help <command>` instead + const QStringList helpSwitches = {QStringLiteral("-h"), QStringLiteral("--help")}; + if (auto it = std::find_first_of( + commandLine.begin(), commandLine.end(), + helpSwitches.begin(), helpSwitches.end()); + it != commandLine.end()) { + command = commandPool.getCommand(HelpCommandType); + commandLine = QList{commandName}; // keep only command's name + } } else { // No command given. if (commandLine.front() == QLatin1String("-h") || commandLine.front() == QLatin1String("--help")) { @@ -408,7 +424,7 @@ QString CommandLineParser::CommandLineParserPrivate::generalHelp() const for (const Command * command : commands) commandMap.insert(command->representation(), command); - for (const Command * command : qAsConst(commandMap)) { + for (const Command * command : std::as_const(commandMap)) { help.append(QLatin1String(" ")).append(command->representation()); const QString whitespace = QString(rhsIndentation - 2 - command->representation().size(), QLatin1Char(' ')); @@ -419,7 +435,7 @@ QString CommandLineParser::CommandLineParserPrivate::generalHelp() const toolNames.sort(); if (!toolNames.empty()) { help.append(QLatin1Char('\n')).append(Tr::tr("Auxiliary commands:\n")); - for (const QString &toolName : qAsConst(toolNames)) { + for (const QString &toolName : std::as_const(toolNames)) { help.append(QLatin1String(" ")).append(toolName); const QString whitespace = QString(rhsIndentation - 2 - toolName.size(), QLatin1Char(' ')); @@ -502,7 +518,7 @@ void CommandLineParser::CommandLineParserPrivate::setupBuildConfigurations() const QVariantMap globalProperties = propertiesPerConfiguration.takeFirst().second; QList<QVariantMap> buildConfigs; - for (const PropertyListItem &item : qAsConst(propertiesPerConfiguration)) { + for (const PropertyListItem &item : std::as_const(propertiesPerConfiguration)) { QVariantMap properties = item.second; for (QVariantMap::ConstIterator globalPropIt = globalProperties.constBegin(); globalPropIt != globalProperties.constEnd(); ++globalPropIt) { diff --git a/src/app/qbs/parser/commandlineparser.h b/src/app/qbs/parser/commandlineparser.h index 70586b2d4..4df8829a2 100644 --- a/src/app/qbs/parser/commandlineparser.h +++ b/src/app/qbs/parser/commandlineparser.h @@ -41,6 +41,8 @@ #include "commandtype.h" +#include <tools/deprecationwarningmode.h> + #include <QtCore/qstringlist.h> #include <QtCore/qvariant.h> @@ -73,12 +75,12 @@ public: CleanOptions cleanOptions(const QString &profile) const; GenerateOptions generateOptions() const; InstallOptions installOptions(const QString &profile) const; + int jobCount(const QString &profile) const; bool forceTimestampCheck() const; bool forceOutputCheck() const; bool dryRun() const; bool forceProbesExecution() const; bool waitLockBuildGraph() const; - bool disableFallbackProvider() const; bool logTime() const; bool withNonDefaultProducts() const; bool buildBeforeInstalling() const; @@ -89,6 +91,7 @@ public: bool showProgress() const; bool showVersion() const; QString settingsDir() const; + DeprecationWarningMode deprecationWarningMode() const; private: class CommandLineParserPrivate; diff --git a/src/app/qbs/parser/parser.pri b/src/app/qbs/parser/parser.pri deleted file mode 100644 index f708f1135..000000000 --- a/src/app/qbs/parser/parser.pri +++ /dev/null @@ -1,16 +0,0 @@ -SOURCES += \ - $$PWD/commandlineparser.cpp \ - $$PWD/commandpool.cpp \ - $$PWD/commandlineoption.cpp \ - $$PWD/commandlineoptionpool.cpp \ - $$PWD/parsercommand.cpp - -HEADERS += \ - $$PWD/commandlineparser.h \ - $$PWD/commandpool.h \ - $$PWD/commandlineoption.h \ - $$PWD/commandlineoptionpool.h \ - $$PWD/commandtype.h \ - $$PWD/parsercommand.h - -include(../../../../qbs_version.pri) diff --git a/src/app/qbs/parser/parsercommand.cpp b/src/app/qbs/parser/parsercommand.cpp index 799bf5dcf..ef8da9551 100644 --- a/src/app/qbs/parser/parsercommand.cpp +++ b/src/app/qbs/parser/parsercommand.cpp @@ -164,7 +164,7 @@ QString Command::supportedOptionsDescription() const } QString s = Tr::tr("The possible options are:\n"); - for (const CommandLineOption *option : qAsConst(optionMap)) + for (const CommandLineOption *option : std::as_const(optionMap)) s += option->description(type()); return s; } @@ -199,16 +199,18 @@ QString ResolveCommand::representation() const static QList<CommandLineOption::Type> resolveOptions() { - return {CommandLineOption::FileOptionType, - CommandLineOption::BuildDirectoryOptionType, - CommandLineOption::LogLevelOptionType, - CommandLineOption::VerboseOptionType, - CommandLineOption::QuietOptionType, - CommandLineOption::ShowProgressOptionType, - CommandLineOption::DryRunOptionType, - CommandLineOption::ForceProbesOptionType, - CommandLineOption::LogTimeOptionType, - CommandLineOption::DisableFallbackProviderType}; + return { + CommandLineOption::FileOptionType, + CommandLineOption::BuildDirectoryOptionType, + CommandLineOption::LogLevelOptionType, + CommandLineOption::VerboseOptionType, + CommandLineOption::QuietOptionType, + CommandLineOption::ShowProgressOptionType, + CommandLineOption::DryRunOptionType, + CommandLineOption::ForceProbesOptionType, + CommandLineOption::LogTimeOptionType, + CommandLineOption::DeprecationWarningsOptionType, + CommandLineOption::JobsOptionType}; } QList<CommandLineOption::Type> ResolveCommand::supportedOptions() const @@ -278,7 +280,6 @@ static QList<CommandLineOption::Type> buildOptions() << CommandLineOption::ForceTimestampCheckOptionType << CommandLineOption::ForceOutputCheckOptionType << CommandLineOption::BuildNonDefaultOptionType - << CommandLineOption::JobsOptionType << CommandLineOption::CommandEchoModeOptionType << CommandLineOption::NoInstallOptionType << CommandLineOption::RemoveFirstOptionType diff --git a/src/app/qbs/qbs.pro b/src/app/qbs/qbs.pro deleted file mode 100644 index 6ce449aad..000000000 --- a/src/app/qbs/qbs.pro +++ /dev/null @@ -1,54 +0,0 @@ -include(../app.pri) -include(parser/parser.pri) - -TARGET = qbs - -SOURCES += main.cpp \ - ctrlchandler.cpp \ - application.cpp \ - session.cpp \ - sessionpacket.cpp \ - sessionpacketreader.cpp \ - stdinreader.cpp \ - status.cpp \ - consoleprogressobserver.cpp \ - commandlinefrontend.cpp \ - qbstool.cpp - -HEADERS += \ - ctrlchandler.h \ - application.h \ - session.h \ - sessionpacket.h \ - sessionpacketreader.h \ - stdinreader.h \ - status.h \ - consoleprogressobserver.h \ - commandlinefrontend.h \ - qbstool.h - -include(../../library_dirname.pri) -isEmpty(QBS_RELATIVE_LIBEXEC_PATH) { - win32:QBS_RELATIVE_LIBEXEC_PATH=. - else:QBS_RELATIVE_LIBEXEC_PATH=../libexec/qbs -} -isEmpty(QBS_RELATIVE_PLUGINS_PATH):QBS_RELATIVE_PLUGINS_PATH=../$${QBS_LIBRARY_DIRNAME} -isEmpty(QBS_RELATIVE_SEARCH_PATH):QBS_RELATIVE_SEARCH_PATH=.. -DEFINES += QBS_RELATIVE_LIBEXEC_PATH=\\\"$${QBS_RELATIVE_LIBEXEC_PATH}\\\" -DEFINES += QBS_RELATIVE_PLUGINS_PATH=\\\"$${QBS_RELATIVE_PLUGINS_PATH}\\\" -DEFINES += QBS_RELATIVE_SEARCH_PATH=\\\"$${QBS_RELATIVE_SEARCH_PATH}\\\" - -CONFIG(static, static|shared) { - include(../../plugins/qbs_plugin_common.pri) - LIBS += -L$$qbsPluginDestDir - scannerPlugins = cpp qt - for (scannerPlugin, scannerPlugins) { - include(../../plugins/scanner/$$scannerPlugin/$${scannerPlugin}.pri) \ - include(../../plugins/use_plugin.pri) - } - generatorPlugins = clangcompilationdb iarew keiluv makefilegenerator visualstudio - for (generatorPlugin, generatorPlugins) { - include(../../plugins/generator/$$generatorPlugin/$${generatorPlugin}.pri) \ - include(../../plugins/use_plugin.pri) - } -} diff --git a/src/app/qbs/qbs.qbs b/src/app/qbs/qbs.qbs index 03685dd21..52c29e9b2 100644 --- a/src/app/qbs/qbs.qbs +++ b/src/app/qbs/qbs.qbs @@ -2,12 +2,16 @@ import qbs.Utilities QbsApp { name: "qbs_app" - Depends { name: "qbs resources" } targetName: "qbs" + + Depends { name: "qbs resources" } + Depends { name: "qtclsp" } + Depends { name: "Qt.network" } Depends { condition: Qt.core.staticBuild || qbsbuildconfig.staticBuild productTypes: ["qbsplugin"] } + cpp.defines: base.concat([ "QBS_VERSION=" + Utilities.cStringQuote(qbsversion.version), "QBS_RELATIVE_LIBEXEC_PATH=" + Utilities.cStringQuote(qbsbuildconfig.relativeLibexecPath), @@ -54,5 +58,13 @@ QbsApp { "parsercommand.h", ] } + Group { + name: "lsp" + cpp.defines: outer.filter(function(d) { return d !== "QT_NO_CAST_FROM_ASCII"; }) + files: [ + "lspserver.cpp", + "lspserver.h", + ] + } } diff --git a/src/app/qbs/session.cpp b/src/app/qbs/session.cpp index 5a1fe145d..2cdcf2b63 100644 --- a/src/app/qbs/session.cpp +++ b/src/app/qbs/session.cpp @@ -39,6 +39,7 @@ #include "session.h" +#include "lspserver.h" #include "sessionpacket.h" #include "sessionpacketreader.h" @@ -66,7 +67,6 @@ #include <QtCore/qobject.h> #include <QtCore/qprocess.h> -#include <algorithm> #include <cstdlib> #include <iostream> #include <memory> @@ -166,6 +166,7 @@ private: FileUpdateData prepareFileUpdate(const QJsonObject &request); SessionPacketReader m_packetReader; + LspServer m_lspServer; Project m_project; ProjectData m_projectData; SessionLogSink m_logSink; @@ -193,7 +194,7 @@ Session::Session() qApp->exit(EXIT_FAILURE); } #endif - sendPacket(SessionPacket::helloMessage()); + sendPacket(SessionPacket::helloMessage(m_lspServer.socketPath())); connect(&m_logSink, &SessionLogSink::newMessage, this, &Session::sendPacket); connect(&m_packetReader, &SessionPacketReader::errorOccurred, this, [](const QString &msg) { @@ -287,6 +288,7 @@ void Session::setupProject(const QJsonObject &request) const ProjectData oldProjectData = m_projectData; m_project = setupJob->project(); m_projectData = m_project.projectData(); + m_lspServer.updateProjectData(m_projectData, m_project.codeLinks()); QJsonObject reply; reply.insert(StringConstants::type(), QLatin1String("project-resolved")); if (success) @@ -554,7 +556,7 @@ void Session::getGeneratedFilesForSources(const QJsonObject &request) reply.insert(StringConstants::type(), QLatin1String(replyType)); const QJsonArray specs = request.value(StringConstants::productsKey()).toArray(); QJsonArray resultProducts; - for (const QJsonValue &p : specs) { + for (const auto &p : specs) { const QJsonObject productObject = p.toObject(); const ProductData product = getProductByName( productObject.value(StringConstants::fullDisplayNameKey()).toString()); @@ -564,7 +566,7 @@ void Session::getGeneratedFilesForSources(const QJsonObject &request) resultProduct.insert(StringConstants::fullDisplayNameKey(), product.fullDisplayName()); QJsonArray results; const QJsonArray requests = productObject.value(QLatin1String("requests")).toArray(); - for (const QJsonValue &r : requests) { + for (const auto &r : requests) { const QJsonObject request = r.toObject(); const QString filePath = request.value(QLatin1String("source-file")).toString(); const QStringList tags = fromJson<QStringList>(request.value(QLatin1String("tags"))); @@ -621,10 +623,11 @@ Session::ProductSelection Session::getProductSelection(const QJsonObject &reques { const QJsonValue productSelection = request.value(StringConstants::productsKey()); if (productSelection.isArray()) - return ProductSelection(getProductsByName(fromJson<QStringList>(productSelection))); - return ProductSelection(productSelection.toString() == QLatin1String("all") - ? Project::ProductSelectionWithNonDefault - : Project::ProductSelectionDefaultOnly); + return {getProductsByName(fromJson<QStringList>(productSelection))}; + return { + productSelection.toString() == QLatin1String("all") + ? Project::ProductSelectionWithNonDefault + : Project::ProductSelectionDefaultOnly}; } Session::FileUpdateData Session::prepareFileUpdate(const QJsonObject &request) @@ -646,7 +649,7 @@ Session::FileUpdateData Session::prepareFileUpdate(const QJsonObject &request) data.error = tr("Product '%1' not found in project.").arg(productName); } const QJsonArray filesArray = request.value(QLatin1String("files")).toArray(); - for (const QJsonValue &v : filesArray) + for (const auto &v : filesArray) data.filePaths << v.toString(); if (m_currentJob) data.error = tr("Cannot update the list of source files while a job is running."); diff --git a/src/app/qbs/sessionpacket.cpp b/src/app/qbs/sessionpacket.cpp index dd6d1726e..470e27091 100644 --- a/src/app/qbs/sessionpacket.cpp +++ b/src/app/qbs/sessionpacket.cpp @@ -82,7 +82,7 @@ SessionPacket::Status SessionPacket::parseInput(QByteArray &input) QJsonObject SessionPacket::retrievePacket() { QBS_ASSERT(isComplete(), return QJsonObject()); - const auto packet = QJsonDocument::fromJson(QByteArray::fromBase64(m_payload)).object(); + auto packet = QJsonDocument::fromJson(QByteArray::fromBase64(m_payload)).object(); m_payload.clear(); m_expectedPayloadLength = -1; return packet; @@ -95,12 +95,13 @@ QByteArray SessionPacket::createPacket(const QJsonObject &packet) .append(jsonData); } -QJsonObject SessionPacket::helloMessage() +QJsonObject SessionPacket::helloMessage(const QString &lspSocket) { return QJsonObject{ {StringConstants::type(), QLatin1String("hello")}, - {QLatin1String("api-level"), 2}, - {QLatin1String("api-compat-level"), 2} + {QLatin1String("api-level"), 5}, + {QLatin1String("api-compat-level"), 2}, + {QLatin1String("lsp-socket"), lspSocket} }; } diff --git a/src/app/qbs/sessionpacket.h b/src/app/qbs/sessionpacket.h index d919ff340..e77b30b75 100644 --- a/src/app/qbs/sessionpacket.h +++ b/src/app/qbs/sessionpacket.h @@ -55,7 +55,7 @@ public: QJsonObject retrievePacket(); static QByteArray createPacket(const QJsonObject &packet); - static QJsonObject helloMessage(); + static QJsonObject helloMessage(const QString &lspSocket); private: bool isComplete() const; diff --git a/src/app/qbs/sessionpacketreader.cpp b/src/app/qbs/sessionpacketreader.cpp index e99ea01ed..daba30d7e 100644 --- a/src/app/qbs/sessionpacketreader.cpp +++ b/src/app/qbs/sessionpacketreader.cpp @@ -42,6 +42,8 @@ #include "sessionpacket.h" #include "stdinreader.h" +#include <QPointer> + namespace qbs { namespace Internal { @@ -64,8 +66,13 @@ void SessionPacketReader::start() StdinReader * const stdinReader = StdinReader::create(this); connect(stdinReader, &StdinReader::errorOccurred, this, &SessionPacketReader::errorOccurred); connect(stdinReader, &StdinReader::dataAvailable, this, [this](const QByteArray &data) { + /* Because this SessionPacketReader can be destroyed in the emit packetReceived, + * use a `QPointer self(this)` to check whether this instance still exists. + * When self evaluates to false, this instance should no longer be referenced, + * so the parent QObject and d should no longer be used in any way. */ + QPointer self(this); d->incomingData += data; - while (!d->incomingData.isEmpty()) { + while (self && !d->incomingData.isEmpty()) { switch (d->currentPacket.parseInput(d->incomingData)) { case SessionPacket::Status::Invalid: emit errorOccurred(tr("Received invalid input.")); diff --git a/src/app/qbs/status.cpp b/src/app/qbs/status.cpp index 127d26a50..8ee39e46f 100644 --- a/src/app/qbs/status.cpp +++ b/src/app/qbs/status.cpp @@ -143,9 +143,8 @@ int printStatus(const ProjectData &project) qbsInfo() << " Group: " << group.name() << " (" << group.location().filePath() << ":" << group.location().line() << ")"; - QStringList sourceFiles = group.allFilePaths(); - std::sort(sourceFiles.begin(), sourceFiles.end()); - for (const QString &sourceFile : qAsConst(sourceFiles)) { + const QStringList sourceFiles = Internal::sorted(group.allFilePaths()); + for (const QString &sourceFile : sourceFiles) { if (!QFileInfo::exists(sourceFile)) missingFiles.push_back(sourceFile); qbsInfo() << " " << sourceFile.mid(projectDirectoryPathLength + 1); @@ -155,11 +154,11 @@ int printStatus(const ProjectData &project) } qbsInfo() << "\nMissing files:"; - for (const QString &untrackedFile : qAsConst(missingFiles)) + for (const QString &untrackedFile : std::as_const(missingFiles)) qbsInfo() << " " << untrackedFile.mid(projectDirectoryPathLength + 1); qbsInfo() << "\nUntracked files:"; - for (const QString &missingFile : qAsConst(untrackedFilesInProject)) + for (const QString &missingFile : std::as_const(untrackedFilesInProject)) qbsInfo() << " " << missingFile.mid(projectDirectoryPathLength + 1); return 0; diff --git a/src/app/qbs/stdinreader.cpp b/src/app/qbs/stdinreader.cpp index 5f00d7de4..4708ff53c 100644 --- a/src/app/qbs/stdinreader.cpp +++ b/src/app/qbs/stdinreader.cpp @@ -43,6 +43,7 @@ #include <QtCore/qfile.h> #include <QtCore/qsocketnotifier.h> +#include <QtCore/qthread.h> #include <QtCore/qtimer.h> #include <cerrno> @@ -111,46 +112,183 @@ public: WindowsStdinReader(QObject *parent) : StdinReader(parent) {} private: - void start() override - { #ifdef Q_OS_WIN32 - m_stdinHandle = GetStdHandle(STD_INPUT_HANDLE); - if (!m_stdinHandle) { - emit errorOccurred(tr("Failed to create handle for standard input.")); - return; + class FileReaderThread : public QThread + { + public: + FileReaderThread(WindowsStdinReader &parent, HANDLE stdInHandle, HANDLE exitEventHandle) + : QThread(&parent), m_stdIn{stdInHandle}, m_exitEvent{exitEventHandle} { } + ~FileReaderThread() + { + wait(); + CloseHandle(m_exitEvent); + } + + void run() override + { + WindowsStdinReader *r = static_cast<WindowsStdinReader *>(parent()); + + char buf[1024]; + while (true) { + DWORD bytesRead = 0; + if (!ReadFile(m_stdIn, buf, sizeof buf, &bytesRead, nullptr)) { + emit r->errorOccurred(tr("Failed to read from input channel.")); + break; + } + if (!bytesRead) + break; + emit r->dataAvailable(QByteArray(buf, bytesRead)); + } + } + private: + HANDLE m_stdIn; + HANDLE m_exitEvent; + }; + + class ConsoleReaderThread : public QThread + { + public: + ConsoleReaderThread(WindowsStdinReader &parent, HANDLE stdInHandle, HANDLE exitEventHandle) + : QThread(&parent), m_stdIn{stdInHandle}, m_exitEvent{exitEventHandle} { } + virtual ~ConsoleReaderThread() override + { + SetEvent(m_exitEvent); + wait(); + CloseHandle(m_exitEvent); } - // A timer seems slightly less awful than to block in a thread - // (how would we abort that one?), but ideally we'd like - // to have a signal-based approach like in the Unix variant. - const auto timer = new QTimer(this); - connect(timer, &QTimer::timeout, this, [this, timer] { + void run() override + { + WindowsStdinReader *r = static_cast<WindowsStdinReader *>(parent()); + + DWORD origConsoleMode; + GetConsoleMode(m_stdIn, &origConsoleMode); + DWORD consoleMode = ENABLE_PROCESSED_INPUT; + SetConsoleMode(m_stdIn, consoleMode); + + HANDLE handles[2] = {m_exitEvent, m_stdIn}; char buf[1024]; - DWORD bytesAvail; - if (!PeekNamedPipe(m_stdinHandle, nullptr, 0, nullptr, &bytesAvail, nullptr)) { - timer->stop(); - emit errorOccurred(tr("Failed to read from input channel.")); + while (true) { + auto result = WaitForMultipleObjects(2, handles, FALSE, INFINITE); + if (result == WAIT_OBJECT_0) + break; + INPUT_RECORD consoleInput; + DWORD inputsRead = 0; + if (!PeekConsoleInputA(m_stdIn, &consoleInput, 1, &inputsRead)) { + emit r->errorOccurred(tr("Failed to read from input channel.")); + break; + } + if (inputsRead) { + if (consoleInput.EventType != KEY_EVENT + || !consoleInput.Event.KeyEvent.bKeyDown + || !consoleInput.Event.KeyEvent.uChar.AsciiChar) { + if (!ReadConsoleInputA(m_stdIn, &consoleInput, 1, &inputsRead)) { + emit r->errorOccurred(tr("Failed to read console input.")); + break; + } + } else { + DWORD bytesRead = 0; + if (!ReadConsoleA(m_stdIn, buf, sizeof buf, &bytesRead, nullptr)) { + emit r->errorOccurred(tr("Failed to read console.")); + break; + } + emit r->dataAvailable(QByteArray(buf, bytesRead)); + } + } + } + SetConsoleMode(m_stdIn, origConsoleMode); + } + private: + HANDLE m_stdIn; + HANDLE m_exitEvent; + }; + + class PipeReaderThread : public QThread + { + public: + PipeReaderThread(WindowsStdinReader &parent, HANDLE stdInHandle, HANDLE exitEventHandle) + : QThread(&parent), m_stdIn{stdInHandle}, m_exitEvent{exitEventHandle} { } + virtual ~PipeReaderThread() override + { + SetEvent(m_exitEvent); + wait(); + CloseHandle(m_exitEvent); + } + + void run() override + { + WindowsStdinReader *r = static_cast<WindowsStdinReader *>(parent()); + + OVERLAPPED overlapped = {}; + overlapped.hEvent = CreateEventA(NULL, TRUE, TRUE, NULL); + if (!overlapped.hEvent) { + emit r->errorOccurred(StdinReader::tr("Failed to create handle for overlapped event.")); return; } - while (bytesAvail > 0) { - DWORD bytesRead; - if (!ReadFile(m_stdinHandle, buf, std::min<DWORD>(bytesAvail, sizeof buf), - &bytesRead, nullptr)) { - timer->stop(); - emit errorOccurred(tr("Failed to read from input channel.")); - return; + + char buf[1024]; + DWORD bytesRead; + HANDLE handles[2] = {m_exitEvent, overlapped.hEvent}; + while (true) { + bytesRead = 0; + auto readResult = ReadFile(m_stdIn, buf, sizeof buf, NULL, &overlapped); + if (!readResult) { + if (GetLastError() != ERROR_IO_PENDING) { + emit r->errorOccurred(StdinReader::tr("ReadFile Failed.")); + break; + } + + auto result = WaitForMultipleObjects(2, handles, FALSE, INFINITE); + if (result == WAIT_OBJECT_0) + break; + } + if (!GetOverlappedResult(m_stdIn, &overlapped, &bytesRead, FALSE)) { + if (GetLastError() != ERROR_HANDLE_EOF) + emit r->errorOccurred(StdinReader::tr("Error GetOverlappedResult.")); + break; } - emit dataAvailable(QByteArray(buf, bytesRead)); - bytesAvail -= bytesRead; + emit r->dataAvailable(QByteArray(buf, bytesRead)); } - }); - timer->start(10); + CancelIo(m_stdIn); + CloseHandle(overlapped.hEvent); + } + private: + HANDLE m_stdIn; + HANDLE m_exitEvent; + }; #endif - } + void start() override + { #ifdef Q_OS_WIN32 - HANDLE m_stdinHandle; + HANDLE stdInHandle = GetStdHandle(STD_INPUT_HANDLE); + if (!stdInHandle) { + emit errorOccurred(StdinReader::tr("Failed to create handle for standard input.")); + return; + } + HANDLE exitEventHandle = CreateEventA(NULL, TRUE, FALSE, NULL); + if (!exitEventHandle) { + emit errorOccurred(StdinReader::tr("Failed to create handle for exit event.")); + return; + } + + auto result = GetFileType(stdInHandle); + switch (result) { + case FILE_TYPE_CHAR: + (new ConsoleReaderThread(*this, stdInHandle, exitEventHandle))->start(); + return; + case FILE_TYPE_PIPE: + (new PipeReaderThread(*this, stdInHandle, exitEventHandle))->start(); + return; + case FILE_TYPE_DISK: + (new FileReaderThread(*this, stdInHandle, exitEventHandle))->start(); + return; + default: + emit errorOccurred(StdinReader::tr("Unable to handle unknown input type")); + return; + } #endif + } }; StdinReader *StdinReader::create(QObject *parent) diff --git a/src/app/shared/logging/logging.pri b/src/app/shared/logging/logging.pri deleted file mode 100644 index e24f33e10..000000000 --- a/src/app/shared/logging/logging.pri +++ /dev/null @@ -1,2 +0,0 @@ -HEADERS += $$PWD/consolelogger.h $$PWD/coloredoutput.h -SOURCES += $$PWD/consolelogger.cpp $$PWD/coloredoutput.cpp |