diff options
Diffstat (limited to 'src/app/qbs')
-rw-r--r-- | src/app/qbs/CMakeLists.txt | 8 | ||||
-rw-r--r-- | src/app/qbs/commandlinefrontend.cpp | 45 | ||||
-rw-r--r-- | src/app/qbs/lspserver.cpp | 629 | ||||
-rw-r--r-- | src/app/qbs/lspserver.h | 65 | ||||
-rw-r--r-- | src/app/qbs/parser/commandlineoption.cpp | 13 | ||||
-rw-r--r-- | src/app/qbs/parser/commandlineoption.h | 17 | ||||
-rw-r--r-- | src/app/qbs/parser/commandlineoptionpool.cpp | 9 | ||||
-rw-r--r-- | src/app/qbs/parser/commandlineoptionpool.h | 1 | ||||
-rw-r--r-- | src/app/qbs/parser/commandlineparser.cpp | 47 | ||||
-rw-r--r-- | src/app/qbs/parser/commandlineparser.h | 2 | ||||
-rw-r--r-- | src/app/qbs/parser/parsercommand.cpp | 26 | ||||
-rw-r--r-- | src/app/qbs/qbs.qbs | 14 | ||||
-rw-r--r-- | src/app/qbs/session.cpp | 15 | ||||
-rw-r--r-- | src/app/qbs/sessionpacket.cpp | 7 | ||||
-rw-r--r-- | src/app/qbs/sessionpacket.h | 2 | ||||
-rw-r--r-- | src/app/qbs/sessionpacketreader.cpp | 9 | ||||
-rw-r--r-- | src/app/qbs/status.cpp | 9 | ||||
-rw-r--r-- | src/app/qbs/stdinreader.cpp | 194 |
18 files changed, 982 insertions, 130 deletions
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) |