aboutsummaryrefslogtreecommitdiffstats
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/config/configcommandexecutor.cpp85
-rw-r--r--src/app/config/configcommandlineparser.cpp2
-rw-r--r--src/app/qbs-create-project/create-project-main.cpp4
-rw-r--r--src/app/qbs-create-project/createproject.cpp8
-rw-r--r--src/app/qbs-setup-android/android-setup.cpp2
-rw-r--r--src/app/qbs-setup-qt/main.cpp2
-rw-r--r--src/app/qbs-setup-toolchains/clangclprobe.cpp20
-rw-r--r--src/app/qbs-setup-toolchains/gccprobe.cpp4
-rw-r--r--src/app/qbs-setup-toolchains/msvcprobe.cpp4
-rw-r--r--src/app/qbs-setup-toolchains/xcodeprobe.cpp8
-rw-r--r--src/app/qbs/CMakeLists.txt8
-rw-r--r--src/app/qbs/commandlinefrontend.cpp45
-rw-r--r--src/app/qbs/lspserver.cpp629
-rw-r--r--src/app/qbs/lspserver.h65
-rw-r--r--src/app/qbs/parser/commandlineoption.cpp13
-rw-r--r--src/app/qbs/parser/commandlineoption.h17
-rw-r--r--src/app/qbs/parser/commandlineoptionpool.cpp9
-rw-r--r--src/app/qbs/parser/commandlineoptionpool.h1
-rw-r--r--src/app/qbs/parser/commandlineparser.cpp47
-rw-r--r--src/app/qbs/parser/commandlineparser.h2
-rw-r--r--src/app/qbs/parser/parsercommand.cpp26
-rw-r--r--src/app/qbs/qbs.qbs14
-rw-r--r--src/app/qbs/session.cpp15
-rw-r--r--src/app/qbs/sessionpacket.cpp7
-rw-r--r--src/app/qbs/sessionpacket.h2
-rw-r--r--src/app/qbs/sessionpacketreader.cpp9
-rw-r--r--src/app/qbs/status.cpp9
-rw-r--r--src/app/qbs/stdinreader.cpp194
28 files changed, 1084 insertions, 167 deletions
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 173676d6d..1ad0cc4b1 100644
--- a/src/app/config/configcommandlineparser.cpp
+++ b/src/app/config/configcommandlineparser.cpp
@@ -126,7 +126,7 @@ 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 (const auto &varName : qAsConst(m_command.varNames)) {
+ for (const auto &varName : std::as_const(m_command.varNames)) {
if (varName.isEmpty())
throw Error(Tr::tr("Property names must not be empty."));
}
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-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-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-toolchains/clangclprobe.cpp b/src/app/qbs-setup-toolchains/clangclprobe.cpp
index 094f8aa14..272f953fc 100644
--- a/src/app/qbs-setup-toolchains/clangclprobe.cpp
+++ b/src/app/qbs-setup-toolchains/clangclprobe.cpp
@@ -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/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 e189dd164..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));
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 c3269ebcf..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,7 +152,6 @@ 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);
@@ -184,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);
@@ -349,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);
}
}
@@ -432,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 {
@@ -490,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);
@@ -504,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();
@@ -565,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()),
@@ -610,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);
}
@@ -648,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);
@@ -672,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 ddbcd4da1..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")
@@ -710,17 +710,6 @@ QString WaitLockOption::longRepresentation() const
return QStringLiteral("--wait-lock");
}
-QString DisableFallbackProviderOption::description(CommandType) const
-{
- return Tr::tr("%1\n\tDo not fall back to pkg-config if a dependency is not found.\n")
- .arg(longRepresentation());
-}
-
-QString DisableFallbackProviderOption::longRepresentation() const
-{
- return QStringLiteral("--no-fallback-module-provider");
-}
-
QString RunEnvConfigOption::description(CommandType command) const
{
Q_UNUSED(command);
diff --git a/src/app/qbs/parser/commandlineoption.h b/src/app/qbs/parser/commandlineoption.h
index d51d5f765..7b8baeae2 100644
--- a/src/app/qbs/parser/commandlineoption.h
+++ b/src/app/qbs/parser/commandlineoption.h
@@ -55,7 +55,9 @@ public:
enum Type {
FileOptionType,
BuildDirectoryOptionType,
- LogLevelOptionType, VerboseOptionType, QuietOptionType,
+ LogLevelOptionType,
+ VerboseOptionType,
+ QuietOptionType,
JobsOptionType,
KeepGoingOptionType,
DryRunOptionType,
@@ -64,7 +66,9 @@ public:
ChangedFilesOptionType,
ProductsOptionType,
NoInstallOptionType,
- InstallRootOptionType, RemoveFirstOptionType, NoBuildOptionType,
+ InstallRootOptionType,
+ RemoveFirstOptionType,
+ NoBuildOptionType,
ForceTimestampCheckOptionType,
ForceOutputCheckOptionType,
BuildNonDefaultOptionType,
@@ -76,7 +80,6 @@ public:
GeneratorOptionType,
WaitLockOptionType,
RunEnvConfigOptionType,
- DisableFallbackProviderType,
DeprecationWarningsOptionType,
};
@@ -431,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 3908f262c..692c9c737 100644
--- a/src/app/qbs/parser/commandlineoptionpool.cpp
+++ b/src/app/qbs/parser/commandlineoptionpool.cpp
@@ -128,9 +128,6 @@ 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;
@@ -279,12 +276,6 @@ WaitLockOption *CommandLineOptionPool::waitLockOption() const
return static_cast<WaitLockOption *>(getOption(CommandLineOption::WaitLockOptionType));
}
-DisableFallbackProviderOption *CommandLineOptionPool::disableFallbackProviderOption() const
-{
- return static_cast<DisableFallbackProviderOption *>(
- getOption(CommandLineOption::DisableFallbackProviderType));
-}
-
RunEnvConfigOption *CommandLineOptionPool::runEnvConfigOption() const
{
return static_cast<RunEnvConfigOption *>(getOption(CommandLineOption::RunEnvConfigOptionType));
diff --git a/src/app/qbs/parser/commandlineoptionpool.h b/src/app/qbs/parser/commandlineoptionpool.h
index 5f3c1d87c..022e9fd09 100644
--- a/src/app/qbs/parser/commandlineoptionpool.h
+++ b/src/app/qbs/parser/commandlineoptionpool.h
@@ -77,7 +77,6 @@ public:
RespectProjectJobLimitsOption *respectProjectJobLimitsOption() const;
GeneratorOption *generatorOption() const;
WaitLockOption *waitLockOption() const;
- DisableFallbackProviderOption *disableFallbackProviderOption() const;
RunEnvConfigOption *runEnvConfigOption() const;
DeprecationWarningsOption *deprecationWarningsOption() const;
diff --git a/src/app/qbs/parser/commandlineparser.cpp b/src/app/qbs/parser/commandlineparser.cpp
index c6134ec80..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;
@@ -334,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")) {
@@ -413,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(' '));
@@ -424,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(' '));
@@ -507,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 999027006..4df8829a2 100644
--- a/src/app/qbs/parser/commandlineparser.h
+++ b/src/app/qbs/parser/commandlineparser.h
@@ -75,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;
diff --git a/src/app/qbs/parser/parsercommand.cpp b/src/app/qbs/parser/parsercommand.cpp
index 8fa67e241..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,17 +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::DeprecationWarningsOptionType,
- 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
@@ -279,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.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 c958c88b6..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)
@@ -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)
diff --git a/src/app/qbs/sessionpacket.cpp b/src/app/qbs/sessionpacket.cpp
index 3830704fa..470e27091 100644
--- a/src/app/qbs/sessionpacket.cpp
+++ b/src/app/qbs/sessionpacket.cpp
@@ -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"), 3},
- {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)