diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2024-01-25 13:08:00 +0100 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2024-02-08 10:15:27 +0000 |
commit | f011142307340444e5ad03b8320bc30ba3bb7982 (patch) | |
tree | 345cfb0a7f906ace27734a85e15e190680209cee | |
parent | 358e1759b33d8715124d2eac9dcff34f3172476e (diff) |
Language server: Add completion support
This feature uncovered some sloppiness in our handling of QVariant types,
which has now been fixed.
Task-number: QBS-395
Change-Id: I687cef7470d97fe7887e4a7a1dbe672b2b9c79ec
Reviewed-by: Ivan Komissarov <ABBAPOH@gmail.com>
Reviewed-by: David Schulz <david.schulz@qt.io>
34 files changed, 480 insertions, 117 deletions
diff --git a/src/app/qbs/lspserver.cpp b/src/app/qbs/lspserver.cpp index 807b6fb1d..c6cce6706 100644 --- a/src/app/qbs/lspserver.cpp +++ b/src/app/qbs/lspserver.cpp @@ -39,18 +39,25 @@ #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 @@ -103,6 +110,32 @@ 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: @@ -119,6 +152,7 @@ public: void handleInitializeRequest(); void handleInitializedNotification(); void handleGotoDefinitionRequest(); + void handleCompletionRequest(); void handleShutdownRequest(); void handleDidOpenNotification(); void handleDidChangeNotification(); @@ -130,6 +164,7 @@ public: lsp::BaseMessage currentMessage; QJsonObject messageObject; QLocalSocket *socket = nullptr; + ProjectData projectData; CodeLinks codeLinks; std::unordered_map<QString, Document> documents; @@ -153,8 +188,9 @@ LspServer::LspServer() : d(new Private) LspServer::~LspServer() { delete d; } -void LspServer::updateProjectData(const CodeLinks &codeLinks) +void LspServer::updateProjectData(const ProjectData &projectData, const CodeLinks &codeLinks) { + d->projectData = projectData; d->codeLinks = codeLinks; } @@ -271,6 +307,8 @@ void LspServer::Private::handleCurrentMessage() 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.")); } @@ -291,6 +329,9 @@ void LspServer::Private::handleInitializeRequest() 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); } @@ -353,6 +394,142 @@ void LspServer::Private::handleGotoDefinitionRequest() 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; @@ -425,14 +602,13 @@ void LspServer::Private::handleDidCloseNotification() static int posToOffset(const CodePosition &pos, const QString &doc) { int offset = 0; - int next = 0; - for (int newlines = 0; newlines < pos.line() - 1; ++newlines) { + 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() - 1; + return offset + pos.column(); } bool Document::isPositionUpToDate(const CodePosition &pos) const diff --git a/src/app/qbs/lspserver.h b/src/app/qbs/lspserver.h index 2538789ca..566808309 100644 --- a/src/app/qbs/lspserver.h +++ b/src/app/qbs/lspserver.h @@ -43,7 +43,9 @@ #include <QString> -namespace qbs::Internal { +namespace qbs { +class ProjectData; +namespace Internal { class LspServer { @@ -51,7 +53,7 @@ public: LspServer(); ~LspServer(); - void updateProjectData(const CodeLinks &codeLinks); + void updateProjectData(const ProjectData &projectData, const CodeLinks &codeLinks); QString socketPath() const; private: @@ -59,4 +61,5 @@ private: Private * const d; }; -} // namespace qbs::Internal +} // namespace Internal +} // namespace qbs diff --git a/src/app/qbs/session.cpp b/src/app/qbs/session.cpp index d69a11b24..ebc9015b2 100644 --- a/src/app/qbs/session.cpp +++ b/src/app/qbs/session.cpp @@ -288,7 +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_project.codeLinks()); + m_lspServer.updateProjectData(m_projectData, m_project.codeLinks()); QJsonObject reply; reply.insert(StringConstants::type(), QLatin1String("project-resolved")); if (success) diff --git a/src/lib/corelib/api/projectdata.cpp b/src/lib/corelib/api/projectdata.cpp index 11469ee18..34e679d6d 100644 --- a/src/lib/corelib/api/projectdata.cpp +++ b/src/lib/corelib/api/projectdata.cpp @@ -40,6 +40,7 @@ #include "projectdata_p.h" #include "propertymap_p.h" +#include <language/builtindeclarations.h> #include <language/language.h> #include <language/propertymapinternal.h> #include <loader/loaderutils.h> @@ -49,7 +50,6 @@ #include <tools/qttools.h> #include <tools/stlutils.h> #include <tools/stringconstants.h> -#include <tools/stringconstants.h> #include <QtCore/qdir.h> #include <QtCore/qjsonarray.h> @@ -732,21 +732,16 @@ bool operator==(const ProductData &lhs, const ProductData &rhs) if (!lhs.isValid() && !rhs.isValid()) return true; - return lhs.isValid() == rhs.isValid() - && lhs.name() == rhs.name() - && lhs.targetName() == rhs.targetName() - && lhs.type() == rhs.type() - && lhs.version() == rhs.version() - && lhs.dependencies() == rhs.dependencies() - && lhs.profile() == rhs.profile() - && lhs.multiplexConfigurationId() == rhs.multiplexConfigurationId() - && lhs.location() == rhs.location() - && lhs.groups() == rhs.groups() - && lhs.generatedArtifacts() == rhs.generatedArtifacts() - && lhs.properties() == rhs.properties() - && lhs.moduleProperties() == rhs.moduleProperties() - && lhs.isEnabled() == rhs.isEnabled() - && lhs.isMultiplexed() == rhs.isMultiplexed(); + return lhs.isValid() == rhs.isValid() && lhs.name() == rhs.name() + && lhs.targetName() == rhs.targetName() && lhs.type() == rhs.type() + && lhs.version() == rhs.version() && lhs.dependencies() == rhs.dependencies() + && lhs.profile() == rhs.profile() + && lhs.multiplexConfigurationId() == rhs.multiplexConfigurationId() + && lhs.location() == rhs.location() && lhs.groups() == rhs.groups() + && lhs.generatedArtifacts() == rhs.generatedArtifacts() + && qVariantMapsEqual(lhs.properties(), rhs.properties()) + && lhs.moduleProperties() == rhs.moduleProperties() && lhs.isEnabled() == rhs.isEnabled() + && lhs.isMultiplexed() == rhs.isMultiplexed(); } bool operator!=(const ProductData &lhs, const ProductData &rhs) @@ -954,6 +949,52 @@ QStringList PropertyMap::allProperties() const } /*! + * \brief Returns the names of all modules whose properties can be requested. + */ +QStringList PropertyMap::allModules() const +{ + QStringList modules; + for (auto it = d->m_map->value().constBegin(); it != d->m_map->value().constEnd(); ++it) { + if (it.value().canConvert<QVariantMap>()) + modules << it.key(); + } + return modules; +} + +/*! + * \brief Returns information about all properties of the given module. + */ +QList<PropertyMap::PropertyInfo> PropertyMap::allPropertiesForModule(const QString &module) const +{ + const QVariantMap moduleProps = d->m_map->value().value(module).toMap(); + QList<PropertyInfo> properties; + const auto builtinProps = transformed<QStringList>( + BuiltinDeclarations::instance().declarationsForType(ItemType::Module).properties(), + [](const PropertyDeclaration &decl) { return decl.name(); }); + for (auto it = moduleProps.begin(); it != moduleProps.end(); ++it) { + static const auto getType = [](const QVariant &v) -> QString { + switch (qVariantType(v)) { + case QMetaType::Bool: + return PropertyDeclaration::typeString(PropertyDeclaration::Boolean); + case QMetaType::Int: + return PropertyDeclaration::typeString(PropertyDeclaration::Integer); + case QMetaType::QVariantList: + return PropertyDeclaration::typeString(PropertyDeclaration::VariantList); + case QMetaType::QString: + return PropertyDeclaration::typeString(PropertyDeclaration::String); + case QMetaType::QStringList: + return PropertyDeclaration::typeString(PropertyDeclaration::StringList); + default: + return PropertyDeclaration::typeString(PropertyDeclaration::Variant); + } + }; + properties << PropertyInfo{ + it.key(), getType(it.value()), it.value(), builtinProps.contains(it.key())}; + } + return properties; +} + +/*! * \brief Returns the value of the given property of a product or group. */ QVariant PropertyMap::getProperty(const QString &name) const diff --git a/src/lib/corelib/api/projectdata.h b/src/lib/corelib/api/projectdata.h index 9fe6445c7..4b7bc2803 100644 --- a/src/lib/corelib/api/projectdata.h +++ b/src/lib/corelib/api/projectdata.h @@ -74,6 +74,14 @@ class QBS_EXPORT PropertyMap friend QBS_EXPORT bool operator!=(const PropertyMap &, const PropertyMap &); public: + struct PropertyInfo + { + QString name; + QString type; + QVariant value; + bool isBuiltin = false; + }; + PropertyMap(); PropertyMap(const PropertyMap &other); PropertyMap(PropertyMap &&other) Q_DECL_NOEXCEPT; @@ -83,6 +91,8 @@ public: PropertyMap &operator =(PropertyMap &&other) Q_DECL_NOEXCEPT; QStringList allProperties() const; + QStringList allModules() const; + QList<PropertyInfo> allPropertiesForModule(const QString &module) const; QVariant getProperty(const QString &name) const; QStringList getModulePropertiesAsStringList(const QString &moduleName, diff --git a/src/lib/corelib/buildgraph/buildgraph.cpp b/src/lib/corelib/buildgraph/buildgraph.cpp index d641627e7..10aae5991 100644 --- a/src/lib/corelib/buildgraph/buildgraph.cpp +++ b/src/lib/corelib/buildgraph/buildgraph.cpp @@ -710,7 +710,7 @@ void updateArtifactFromSourceArtifact(const ResolvedProductPtr &product, const QVariantMap oldModuleProperties = artifact->properties->value(); setArtifactData(artifact, sourceArtifact); if (oldFileTags != artifact->fileTags() - || oldModuleProperties != artifact->properties->value()) { + || !qVariantMapsEqual(oldModuleProperties, artifact->properties->value())) { invalidateArtifactAsRuleInputIfNecessary(artifact); } } @@ -766,7 +766,7 @@ void updateGeneratedArtifacts(ResolvedProduct *product) provideFullFileTagsAndProperties(artifact); applyPerArtifactProperties(artifact); if (oldFileTags != artifact->fileTags() - || oldModuleProperties != artifact->properties->value()) { + || !qVariantMapsEqual(oldModuleProperties, artifact->properties->value())) { invalidateArtifactAsRuleInputIfNecessary(artifact); } } diff --git a/src/lib/corelib/buildgraph/buildgraphloader.cpp b/src/lib/corelib/buildgraph/buildgraphloader.cpp index 023931e5c..6488ce9fd 100644 --- a/src/lib/corelib/buildgraph/buildgraphloader.cpp +++ b/src/lib/corelib/buildgraph/buildgraphloader.cpp @@ -755,10 +755,11 @@ bool BuildGraphLoader::checkProductForInstallInfoChanges(const ResolvedProductPt << StringConstants::installDirProperty() << StringConstants::installPrefixProperty() << StringConstants::installRootProperty(); for (const QString &key : specialProperties) { - if (restoredProduct->moduleProperties->qbsPropertyValue(key) - != newlyResolvedProduct->moduleProperties->qbsPropertyValue(key)) { + if (!qVariantsEqual( + restoredProduct->moduleProperties->qbsPropertyValue(key), + newlyResolvedProduct->moduleProperties->qbsPropertyValue(key))) { qCDebug(lcBuildGraph).noquote().nospace() - << "Product property 'qbs." << key << "' changed."; + << "Product property 'qbs." << key << "' changed."; return true; } } @@ -850,7 +851,8 @@ bool BuildGraphLoader::checkConfigCompatibility() } if (!m_parameters.overrideBuildGraphData()) { if (!m_parameters.overriddenValues().empty() - && m_parameters.overriddenValues() != restoredProject->overriddenValues) { + && !qVariantMapsEqual( + m_parameters.overriddenValues(), restoredProject->overriddenValues)) { const auto toUserOutput = [](const QVariantMap &propMap) { QString o; for (auto it = propMap.begin(); it != propMap.end(); ++it) { @@ -868,7 +870,7 @@ bool BuildGraphLoader::checkConfigCompatibility() "you really want to rebuild with the new properties.") .arg(toUserOutput(restoredProject->overriddenValues), toUserOutput(m_parameters.overriddenValues()))); - } + } m_parameters.setOverriddenValues(restoredProject->overriddenValues); if (m_parameters.topLevelProfile() != restoredProject->profile()) { throw ErrorInfo(Tr::tr("The current profile is '%1', but profile '%2' was used " @@ -883,7 +885,8 @@ bool BuildGraphLoader::checkConfigCompatibility() } if (!m_parameters.overrideBuildGraphData()) return true; - if (m_parameters.finalBuildConfigurationTree() != restoredProject->buildConfiguration()) + if (!qVariantMapsEqual( + m_parameters.finalBuildConfigurationTree(), restoredProject->buildConfiguration())) return false; Settings settings(m_parameters.settingsDirectory()); const QVariantMap profileConfigsTree = restoredProject->fullProfileConfigsTree(); diff --git a/src/lib/corelib/buildgraph/rulecommands.cpp b/src/lib/corelib/buildgraph/rulecommands.cpp index 6acd1d68c..73d05eaca 100644 --- a/src/lib/corelib/buildgraph/rulecommands.cpp +++ b/src/lib/corelib/buildgraph/rulecommands.cpp @@ -99,15 +99,11 @@ AbstractCommand::~AbstractCommand() = default; bool AbstractCommand::equals(const AbstractCommand *other) const { - return type() == other->type() - && m_description == other->m_description - && m_extendedDescription == other->m_extendedDescription - && m_highlight == other->m_highlight - && m_ignoreDryRun == other->m_ignoreDryRun - && m_silent == other->m_silent - && m_jobPool == other->m_jobPool - && m_timeout == other->m_timeout - && m_properties == other->m_properties; + return type() == other->type() && m_description == other->m_description + && m_extendedDescription == other->m_extendedDescription + && m_highlight == other->m_highlight && m_ignoreDryRun == other->m_ignoreDryRun + && m_silent == other->m_silent && m_jobPool == other->m_jobPool + && m_timeout == other->m_timeout && qVariantMapsEqual(m_properties, other->m_properties); } void AbstractCommand::fillFromScriptValue(JSContext *ctx, const JSValue *scriptValue, diff --git a/src/lib/corelib/buildgraph/rulesapplicator.cpp b/src/lib/corelib/buildgraph/rulesapplicator.cpp index 5cc4be96e..e07abd74a 100644 --- a/src/lib/corelib/buildgraph/rulesapplicator.cpp +++ b/src/lib/corelib/buildgraph/rulesapplicator.cpp @@ -293,8 +293,9 @@ void RulesApplicator::doApply(const ArtifactSet &inputArtifacts, JSValue prepare } outputArtifact->properties->setValue(artifactModulesCfg); if (!outputInfo.newlyCreated - && (outputArtifact->fileTags() != outputInfo.oldFileTags - || outputArtifact->properties->value() != outputInfo.oldProperties)) { + && (outputArtifact->fileTags() != outputInfo.oldFileTags + || !qVariantMapsEqual( + outputArtifact->properties->value(), outputInfo.oldProperties))) { invalidateArtifactAsRuleInputIfNecessary(outputArtifact); } } @@ -664,8 +665,10 @@ Artifact *RulesApplicator::createOutputArtifactFromScriptValue(const JSValue &ob connect(outputInfo.artifact, dependency); } ArtifactBindingsExtractor().apply(engine(), outputInfo.artifact, obj); - if (!outputInfo.newlyCreated && (outputInfo.artifact->fileTags() != outputInfo.oldFileTags - || outputInfo.artifact->properties->value() != outputInfo.oldProperties)) { + if (!outputInfo.newlyCreated + && (outputInfo.artifact->fileTags() != outputInfo.oldFileTags + || !qVariantMapsEqual( + outputInfo.artifact->properties->value(), outputInfo.oldProperties))) { invalidateArtifactAsRuleInputIfNecessary(outputInfo.artifact); } return outputInfo.artifact; diff --git a/src/lib/corelib/buildgraph/transformerchangetracking.cpp b/src/lib/corelib/buildgraph/transformerchangetracking.cpp index f0b8986f4..ae43e8219 100644 --- a/src/lib/corelib/buildgraph/transformerchangetracking.cpp +++ b/src/lib/corelib/buildgraph/transformerchangetracking.cpp @@ -158,7 +158,7 @@ bool TrafoChangeTracker::checkForPropertyChange(const Property &restoredProperty case Property::PropertyInArtifact: QBS_CHECK(false); } - if (restoredProperty.value != v) { + if (!qVariantsEqual(restoredProperty.value, v)) { qCDebug(lcBuildGraph).noquote().nospace() << "Value for property '" << restoredProperty.moduleName << "." << restoredProperty.propertyName << "' has changed.\n" diff --git a/src/lib/corelib/language/language.cpp b/src/lib/corelib/language/language.cpp index d2c213999..1f491814a 100644 --- a/src/lib/corelib/language/language.cpp +++ b/src/lib/corelib/language/language.cpp @@ -939,7 +939,7 @@ bool operator==(const ExportedProperty &p1, const ExportedProperty &p2) bool operator==(const ExportedModuleDependency &d1, const ExportedModuleDependency &d2) { - return d1.name == d2.name && d1.moduleProperties == d2.moduleProperties; + return d1.name == d2.name && qVariantMapsEqual(d1.moduleProperties, d2.moduleProperties); } bool equals(const std::vector<ExportedItemPtr> &l1, const std::vector<ExportedItemPtr> &l2) @@ -966,20 +966,19 @@ bool operator==(const ExportedModule &m1, const ExportedModule &m2) for (auto it1 = m1.cbegin(), it2 = m2.cbegin(); it1 != m1.cend(); ++it1, ++it2) { if (it1.key()->name != it2.key()->name) return false; - if (it1.value() != it2.value()) + if (!qVariantMapsEqual(it1.value(), it2.value())) return false; } return true; }; - return m1.propertyValues == m2.propertyValues - && m1.modulePropertyValues == m2.modulePropertyValues - && equals(m1.children, m2.children) - && m1.m_properties == m2.m_properties - && m1.importStatements == m2.importStatements - && m1.productDependencies.size() == m2.productDependencies.size() - && m1.productDependencies == m2.productDependencies - && depMapsEqual(m1.dependencyParameters, m2.dependencyParameters); + return qVariantMapsEqual(m1.propertyValues, m2.propertyValues) + && qVariantMapsEqual(m1.modulePropertyValues, m2.modulePropertyValues) + && equals(m1.children, m2.children) && m1.m_properties == m2.m_properties + && m1.importStatements == m2.importStatements + && m1.productDependencies.size() == m2.productDependencies.size() + && m1.productDependencies == m2.productDependencies + && depMapsEqual(m1.dependencyParameters, m2.dependencyParameters); } JSValue PrivateScriptFunction::getFunction(ScriptEngine *engine, const QString &errorMessage) const diff --git a/src/lib/corelib/language/propertydeclaration.cpp b/src/lib/corelib/language/propertydeclaration.cpp index 9b1b890f4..d56ab3bb0 100644 --- a/src/lib/corelib/language/propertydeclaration.cpp +++ b/src/lib/corelib/language/propertydeclaration.cpp @@ -308,6 +308,26 @@ QVariant PropertyDeclaration::convertToPropertyType(const QVariant &v, Type t, return c; } +QVariant PropertyDeclaration::typedNullValue() const +{ + switch (type()) { + case PropertyDeclaration::Boolean: + return typedNullVariant<bool>(); + case PropertyDeclaration::Integer: + return typedNullVariant<int>(); + case PropertyDeclaration::VariantList: + return typedNullVariant<QVariantList>(); + case PropertyDeclaration::String: + case PropertyDeclaration::Path: + return typedNullVariant<QString>(); + case PropertyDeclaration::StringList: + case PropertyDeclaration::PathList: + return typedNullVariant<QStringList>(); + default: + return {}; + } +} + bool PropertyDeclaration::shouldCheckAllowedValues() const { return isValid() diff --git a/src/lib/corelib/language/propertydeclaration.h b/src/lib/corelib/language/propertydeclaration.h index a64094af8..79a39ecbd 100644 --- a/src/lib/corelib/language/propertydeclaration.h +++ b/src/lib/corelib/language/propertydeclaration.h @@ -128,6 +128,7 @@ public: static QVariant convertToPropertyType( const QVariant &v, Type t, const QStringList &namePrefix, const QString &key); + QVariant typedNullValue() const; bool shouldCheckAllowedValues() const; void checkAllowedValues( diff --git a/src/lib/corelib/language/propertymapinternal.h b/src/lib/corelib/language/propertymapinternal.h index 83e18ba48..af551cf6f 100644 --- a/src/lib/corelib/language/propertymapinternal.h +++ b/src/lib/corelib/language/propertymapinternal.h @@ -77,7 +77,7 @@ private: inline bool operator==(const PropertyMapInternal &lhs, const PropertyMapInternal &rhs) { - return lhs.m_value == rhs.m_value; + return qVariantsEqual(lhs.m_value, rhs.m_value); } QVariant QBS_AUTOTEST_EXPORT moduleProperty(const QVariantMap &properties, diff --git a/src/lib/corelib/language/scriptengine.cpp b/src/lib/corelib/language/scriptengine.cpp index d655c0073..7847cb24d 100644 --- a/src/lib/corelib/language/scriptengine.cpp +++ b/src/lib/corelib/language/scriptengine.cpp @@ -488,6 +488,8 @@ void ScriptEngine::addInternalExtension(const char *name, JSValue ext) JSValue ScriptEngine::asJsValue(const QVariant &v, quintptr id, bool frozen) { + if (v.isNull()) + return JS_UNDEFINED; switch (static_cast<QMetaType::Type>(v.userType())) { case QMetaType::QByteArray: return asJsValue(v.toByteArray()); diff --git a/src/lib/corelib/loader/dependenciesresolver.cpp b/src/lib/corelib/loader/dependenciesresolver.cpp index e49af1600..5df47217f 100644 --- a/src/lib/corelib/loader/dependenciesresolver.cpp +++ b/src/lib/corelib/loader/dependenciesresolver.cpp @@ -751,7 +751,7 @@ void DependenciesResolver::adjustDependsItemForMultiplexing(Item *dependsItem) for (auto lhsProperty = lhs.constBegin(); lhsProperty != lhs.constEnd(); lhsProperty++) { const auto rhsProperty = rhs.find(lhsProperty.key()); const bool isCommonProperty = rhsProperty != rhs.constEnd(); - if (isCommonProperty && lhsProperty.value() != rhsProperty.value()) + if (isCommonProperty && !qVariantsEqual(lhsProperty.value(), rhsProperty.value())) return false; } return true; diff --git a/src/lib/corelib/loader/loaderutils.cpp b/src/lib/corelib/loader/loaderutils.cpp index a78b3b4c5..a6105ee50 100644 --- a/src/lib/corelib/loader/loaderutils.cpp +++ b/src/lib/corelib/loader/loaderutils.cpp @@ -943,7 +943,7 @@ void DependencyParametersMerger::merge(QVariantMap ¤t, const QVariantMap & currentValue = mdst; } else { if (m_currentPrio == nextPrio) { - if (currentValue.isValid() && currentValue != newValue) + if (currentValue.isValid() && !qVariantsEqual(currentValue, newValue)) m_conflicts.emplace_back(m_path, currentValue, newValue, m_currentPrio); } else { removeIf(m_conflicts, [this](const Conflict &conflict) { diff --git a/src/lib/corelib/loader/probesresolver.cpp b/src/lib/corelib/loader/probesresolver.cpp index b38366900..efca2854c 100644 --- a/src/lib/corelib/loader/probesresolver.cpp +++ b/src/lib/corelib/loader/probesresolver.cpp @@ -212,7 +212,7 @@ void ProbesResolver::resolveProbe(ProductContext &productContext, Item *parent, newValue = initialProperties.value(b.first); } } - if (newValue != getJsVariant(ctx, b.second)) { + if (!qVariantsEqual(newValue, getJsVariant(ctx, b.second))) { if (!resolvedProbe) storedValue = VariantValue::createStored(newValue); else @@ -281,10 +281,10 @@ bool ProbesResolver::probeMatches(const ProbeConstPtr &probe, bool condition, CompareScript compareScript) const { return probe->condition() == condition - && probe->initialProperties() == initialProperties - && (compareScript == CompareScript::No - || (probe->configureScript() == configureScript - && !probe->needsReconfigure(m_loaderState.topLevelProject().lastResolveTime()))); + && qVariantMapsEqual(probe->initialProperties(), initialProperties) + && (compareScript == CompareScript::No + || (probe->configureScript() == configureScript + && !probe->needsReconfigure(m_loaderState.topLevelProject().lastResolveTime()))); } } // namespace Internal diff --git a/src/lib/corelib/loader/productresolver.cpp b/src/lib/corelib/loader/productresolver.cpp index 9944fc56f..fd2f044a7 100644 --- a/src/lib/corelib/loader/productresolver.cpp +++ b/src/lib/corelib/loader/productresolver.cpp @@ -1589,7 +1589,13 @@ void PropertiesEvaluator::evaluateProperty( } else if (pd.type() == PropertyDeclaration::VariantList) { v = v.toList(); } + + // Enforce proper type for undefined values (note that path degrades to string). + if (!v.isValid()) + v = pd.typedNullValue(); + pd.checkAllowedValues(v, propValue->location(), propName, m_loaderState); + result[propName] = v; break; } diff --git a/src/lib/corelib/parser/qmljsastvisitor_p.h b/src/lib/corelib/parser/qmljsastvisitor_p.h index bec174c65..4b7911aa4 100644 --- a/src/lib/corelib/parser/qmljsastvisitor_p.h +++ b/src/lib/corelib/parser/qmljsastvisitor_p.h @@ -58,7 +58,7 @@ namespace QbsQmlJS { namespace AST { -class QBS_AUTOTEST_EXPORT Visitor +class QML_PARSER_EXPORT Visitor { public: Visitor(); diff --git a/src/lib/corelib/parser/qmljsengine_p.h b/src/lib/corelib/parser/qmljsengine_p.h index 2a616126d..9c603ee5c 100644 --- a/src/lib/corelib/parser/qmljsengine_p.h +++ b/src/lib/corelib/parser/qmljsengine_p.h @@ -93,7 +93,7 @@ public: QString message; }; -class QBS_AUTOTEST_EXPORT Engine +class QML_PARSER_EXPORT Engine { Lexer *_lexer{nullptr}; Directives *_directives{nullptr}; diff --git a/src/lib/corelib/parser/qmljsglobal_p.h b/src/lib/corelib/parser/qmljsglobal_p.h index c3d198ea5..02e0b38e4 100644 --- a/src/lib/corelib/parser/qmljsglobal_p.h +++ b/src/lib/corelib/parser/qmljsglobal_p.h @@ -41,31 +41,14 @@ #include <QtCore/qglobal.h> -// Force QML_PARSER_EXPORT to be always empty. -#ifndef QT_CREATOR -# define QT_CREATOR -#endif -#ifdef QML_BUILD_STATIC_LIB -# undef QML_BUILD_STATIC_LIB -#endif -#define QML_BUILD_STATIC_LIB 1 - -#ifdef QT_CREATOR -# ifdef QMLJS_BUILD_DIR -# define QML_PARSER_EXPORT Q_DECL_EXPORT -# elif QML_BUILD_STATIC_LIB -# define QML_PARSER_EXPORT -# else -# define QML_PARSER_EXPORT Q_DECL_IMPORT -# endif // QMLJS_BUILD_DIR - -#else // !QT_CREATOR -# if defined(QT_BUILD_QMLDEVTOOLS_LIB) || defined(QT_QMLDEVTOOLS_LIB) - // QmlDevTools is a static library -# define QML_PARSER_EXPORT -# else -# define QML_PARSER_EXPORT Q_AUTOTEST_EXPORT -# endif -#endif // QT_CREATOR +#ifdef QBS_STATIC_LIB +#define QML_PARSER_EXPORT +#else +#ifdef QBS_LIBRARY +#define QML_PARSER_EXPORT Q_DECL_EXPORT +#else +#define QML_PARSER_EXPORT Q_DECL_IMPORT +#endif // QBS_LIBRARY +#endif // QBS_STATIC_LIB #endif // QMLJSGLOBAL_P_H diff --git a/src/lib/corelib/parser/qmljslexer_p.h b/src/lib/corelib/parser/qmljslexer_p.h index aef68e0c5..c9801c0f5 100644 --- a/src/lib/corelib/parser/qmljslexer_p.h +++ b/src/lib/corelib/parser/qmljslexer_p.h @@ -86,7 +86,7 @@ public: } }; -class QBS_AUTOTEST_EXPORT Lexer: public QmlJSGrammar +class QML_PARSER_EXPORT Lexer : public QmlJSGrammar { public: enum { diff --git a/src/lib/corelib/parser/qmljsparser_p.h b/src/lib/corelib/parser/qmljsparser_p.h index c761bb25b..9744a7eb6 100644 --- a/src/lib/corelib/parser/qmljsparser_p.h +++ b/src/lib/corelib/parser/qmljsparser_p.h @@ -70,7 +70,7 @@ namespace QbsQmlJS { class Engine; -class QBS_AUTOTEST_EXPORT Parser: protected QmlJSGrammar +class QML_PARSER_EXPORT Parser : protected QmlJSGrammar { public: union Value { diff --git a/src/lib/corelib/tools/persistence.cpp b/src/lib/corelib/tools/persistence.cpp index 44090dee2..0e545377a 100644 --- a/src/lib/corelib/tools/persistence.cpp +++ b/src/lib/corelib/tools/persistence.cpp @@ -140,6 +140,11 @@ void PersistentPool::finalizeWriteStream() void PersistentPool::storeVariant(const QVariant &variant) { + if (variant.isNull()) { + m_stream << quint32(QMetaType::User); + m_stream << variant; + return; + } const auto type = static_cast<quint32>(variant.userType()); m_stream << type; switch (type) { @@ -231,8 +236,5 @@ void PersistentPool::doStoreValue(const QProcessEnvironment &env) store(env.value(key)); } -const PersistentPool::PersistentObjectId PersistentPool::ValueNotFoundId; -const PersistentPool::PersistentObjectId PersistentPool::EmptyValueId; - } // namespace Internal } // namespace qbs diff --git a/src/lib/corelib/tools/persistence.h b/src/lib/corelib/tools/persistence.h index b7aa543a4..86365c993 100644 --- a/src/lib/corelib/tools/persistence.h +++ b/src/lib/corelib/tools/persistence.h @@ -145,8 +145,9 @@ private: template<typename T> QHash<T, PersistentObjectId> &idMap(); template<typename T> PersistentObjectId &lastStoredId(); - static const PersistentObjectId ValueNotFoundId = -1; - static const PersistentObjectId EmptyValueId = -2; + static const inline PersistentObjectId ValueNotFoundId = -1; + static const inline PersistentObjectId EmptyValueId = -2; + static const inline PersistentObjectId NullValueId = -3; std::unique_ptr<QIODevice> m_file; QDataStream m_stream; @@ -271,8 +272,13 @@ template<typename T> inline T PersistentPool::idLoadValue() { int id; m_stream >> id; - if (id == EmptyValueId) + if (id == NullValueId) return T(); + if (id == EmptyValueId) { + if constexpr (std::is_same_v<T, QString>) + return QString(0, QChar()); + return T(); + } QBS_CHECK(id >= 0); if (id >= static_cast<int>(idStorage<T>().size())) { T value; @@ -287,6 +293,12 @@ template<typename T> inline T PersistentPool::idLoadValue() template<typename T> void PersistentPool::idStoreValue(const T &value) { + if constexpr (std::is_same_v<T, QString>) { + if (value.isNull()) { + m_stream << NullValueId; + return; + } + } if (value.isEmpty()) { m_stream << EmptyValueId; return; diff --git a/src/lib/corelib/tools/qttools.h b/src/lib/corelib/tools/qttools.h index 88ada73d4..029948be4 100644 --- a/src/lib/corelib/tools/qttools.h +++ b/src/lib/corelib/tools/qttools.h @@ -199,6 +199,51 @@ inline bool qVariantConvert(QVariant &variant, int typeId) #endif } +inline QMetaType::Type qVariantType(const QVariant &v) +{ + return static_cast<QMetaType::Type>( +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + v.metaType().id() +#else + v.type() +#endif + ); +} + +template<typename T> +inline QVariant typedNullVariant() +{ + const auto metaType = QMetaType::fromType<T>(); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + return QVariant(metaType, nullptr); +#else + return QVariant(static_cast<QVariant::Type>(metaType.id())); +#endif +} + +inline bool qVariantsEqual(const QVariant &v1, const QVariant &v2) +{ + return v1.isNull() == v2.isNull() && v1 == v2; +} + +inline bool qVariantMapsEqual(const QVariantMap &m1, const QVariantMap &m2) +{ + if (m1.size() != m2.size()) + return false; + if (m1.isSharedWith(m2)) + return true; + + auto it1 = m1.cbegin(); + auto it2 = m2.cbegin(); + while (it1 != m1.cend()) { + if (it1.key() != it2.key() || !qVariantsEqual(it1.value(), it2.value())) + return false; + ++it2; + ++it1; + } + return true; +} + } // namespace qbs #endif // QBSQTTOOLS_H diff --git a/src/lib/corelib/tools/setupprojectparameters.cpp b/src/lib/corelib/tools/setupprojectparameters.cpp index 28ad745ce..185323c09 100644 --- a/src/lib/corelib/tools/setupprojectparameters.cpp +++ b/src/lib/corelib/tools/setupprojectparameters.cpp @@ -47,6 +47,7 @@ #include <tools/jsonhelper.h> #include <tools/profile.h> #include <tools/qbsassert.h> +#include <tools/qttools.h> #include <tools/scripttools.h> #include <tools/settings.h> #include <tools/stringconstants.h> @@ -517,7 +518,7 @@ ErrorInfo SetupProjectParameters::expandBuildConfiguration() QVariantMap expandedConfig = expandedBuildConfiguration(profile, configurationName(), &err); if (err.hasError()) return err; - if (d->buildConfiguration != expandedConfig) { + if (!qVariantMapsEqual(d->buildConfiguration, expandedConfig)) { d->buildConfigurationTree.clear(); d->buildConfiguration = expandedConfig; } diff --git a/tests/auto/api/tst_api.cpp b/tests/auto/api/tst_api.cpp index d3141ce59..7726cee15 100644 --- a/tests/auto/api/tst_api.cpp +++ b/tests/auto/api/tst_api.cpp @@ -1925,7 +1925,8 @@ struct ProductDataSelector bool qbsPropertiesMatch(const qbs::ProductData &p) const { for (auto it = qbsProperties.begin(); it != qbsProperties.end(); ++it) { - if (it.value() != p.moduleProperties().getModuleProperty("qbs", it.key())) + if (!qbs::qVariantsEqual( + it.value(), p.moduleProperties().getModuleProperty("qbs", it.key()))) return false; } return true; diff --git a/tests/auto/blackbox/testdata/lsp/lsp.qbs b/tests/auto/blackbox/testdata/lsp/lsp.qbs index 2e30ad930..24479e0ec 100644 --- a/tests/auto/blackbox/testdata/lsp/lsp.qbs +++ b/tests/auto/blackbox/testdata/lsp/lsp.qbs @@ -3,6 +3,7 @@ Project { name: "dep" Depends { name: "m" } Depends { name: "Prefix"; submodules: ["m1", "m2", "m3"] } + } Product { Depends { name: "dep" } diff --git a/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs index 84957060c..09bac2dc2 100644 --- a/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs +++ b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs @@ -1,2 +1,5 @@ Module { + property bool p1 + property string p2 + property bool x } diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp index 11a4078ea..8ba6e2fa5 100644 --- a/tests/auto/blackbox/tst_blackbox.cpp +++ b/tests/auto/blackbox/tst_blackbox.cpp @@ -6369,20 +6369,33 @@ void TestBlackbox::qbsLanguageServer_data() << ((testDataDir + "/lsp/modules/Prefix/m1/m1.qbs:1:1\n") + (testDataDir + "/lsp/modules/Prefix/m2/m2.qbs:1:1\n") + (testDataDir + "/lsp/modules/Prefix/m3/m3.qbs:1:1")); - QTest::addRow("follow to product") << "--goto-def" - << (testDataDir + "/lsp/lsp.qbs:8:19") - << QString() << QString() - << (testDataDir + "/lsp/lsp.qbs:2:5"); + QTest::addRow("follow to product") + << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:9:19") << QString() << QString() + << (testDataDir + "/lsp/lsp.qbs:2:5"); QTest::addRow("follow to module, non-invalidating insert") - << "--goto-def" - << (testDataDir + "/lsp/lsp.qbs:4:9") - << "5:9" << QString("property bool dummy\n") - << (testDataDir + "/lsp/modules/m/m.qbs:1:1"); + << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:4:9") << "5:9" + << QString("property bool dummy\n") << (testDataDir + "/lsp/modules/m/m.qbs:1:1"); QTest::addRow("follow to module, invalidating insert") - << "--goto-def" - << (testDataDir + "/lsp/lsp.qbs:4:9") - << QString() << QString("property bool dummy\n") - << QString(); + << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:4:9") << QString() + << QString("property bool dummy\n") << QString(); + QTest::addRow("completion: LHS, module prefix") + << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() << QString("P") + << QString("Prefix.m1\nPrefix.m2\nPrefix.m3"); + QTest::addRow("completion: LHS, module name") + << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() << QString("Prefix.m") + << QString("m1\nm2\nm3"); + QTest::addRow("completion: LHS, module property right after dot") + << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() + << QString("Prefix.m1.") << QString("p1 bool\np2 string\nx bool"); + QTest::addRow("completion: LHS, module property with identifier prefix") + << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() + << QString("Prefix.m1.p") << QString("p1 bool\np2 string"); + QTest::addRow("completion: simple RHS, module property") + << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() + << QString("property bool dummy: Prefix.m1.p") << QString("p1 bool\np2 string"); + QTest::addRow("completion: complex RHS, module property") + << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() + << QString("property bool dummy: { return Prefix.m1.p") << QString("p1 bool\np2 string"); } void TestBlackbox::qbsLanguageServer() diff --git a/tests/auto/language/tst_language.cpp b/tests/auto/language/tst_language.cpp index 7af4356cb..456e8b9d0 100644 --- a/tests/auto/language/tst_language.cpp +++ b/tests/auto/language/tst_language.cpp @@ -1928,7 +1928,7 @@ void TestLanguage::moduleParameters() }; const QVariantMap actual = findInProduct(it.key()); const QVariantMap expected = it.value().toMap(); - const bool same = actual == expected; + const bool same = qVariantMapsEqual(actual, expected); if (!same) { qDebug().noquote() << "---" << expected; qDebug().noquote() << "+++" << actual; diff --git a/tests/lspclient/lspclient.cpp b/tests/lspclient/lspclient.cpp index 8574161da..40adc9288 100644 --- a/tests/lspclient/lspclient.cpp +++ b/tests/lspclient/lspclient.cpp @@ -27,6 +27,7 @@ ****************************************************************************/ #include <lsp/clientcapabilities.h> +#include <lsp/completion.h> #include <lsp/initializemessages.h> #include <lsp/languagefeatures.h> #include <lsp/textsynchronization.h> @@ -40,7 +41,10 @@ #include <cstdlib> #include <iostream> -enum class Command { GotoDefinition, }; +enum class Command { + GotoDefinition, + Completion, +}; class LspClient : public QObject { @@ -65,6 +69,8 @@ private: void handleResponse(); void sendGotoDefinitionRequest(); void handleGotoDefinitionResponse(); + void sendCompletionRequest(); + void handleCompletionResponse(); lsp::DocumentUri uri() const; lsp::DocumentUri::PathMapper mapper() const; @@ -93,6 +99,8 @@ int main(int argc, char *argv[]) "socket"); const QCommandLineOption gotoDefinitionOption( {"goto-def", "g"}, "Go to definition from the specified location."); + const QCommandLineOption completionOption( + {"completion", "c"}, "Request completion at the specified location."); const QCommandLineOption insertCodeOption("insert-code", "A piece of code to insert before doing the actual " "operation.", @@ -101,7 +109,12 @@ int main(int argc, char *argv[]) "The location at which to insert the code.", "<line>:<column>"); QCommandLineParser parser; - parser.addOptions({socketOption, insertCodeOption, insertLocationOption, gotoDefinitionOption}); + parser.addOptions( + {socketOption, + insertCodeOption, + insertLocationOption, + gotoDefinitionOption, + completionOption}); parser.addHelpOption(); parser.addPositionalArgument("location", "The location at which to operate.", "<file>:<line>:<column>"); @@ -120,6 +133,8 @@ int main(int argc, char *argv[]) if (parser.isSet(gotoDefinitionOption)) command = Command::GotoDefinition; + else if (parser.isSet(completionOption)) + command = Command::Completion; else complainAndExit("Don't know what to do."); @@ -339,6 +354,8 @@ void LspClient::sendRequest() switch (m_command) { case Command::GotoDefinition: return sendGotoDefinitionRequest(); + case Command::Completion: + return sendCompletionRequest(); } } @@ -352,6 +369,8 @@ void LspClient::handleResponse() switch (m_command) { case Command::GotoDefinition: return handleGotoDefinitionResponse(); + case Command::Completion: + return handleCompletionResponse(); } } @@ -380,6 +399,29 @@ void LspClient::handleGotoDefinitionResponse() exit(EXIT_SUCCESS); } +void LspClient::sendCompletionRequest() +{ + const lsp::TextDocumentIdentifier doc(uri()); + const lsp::Position pos(m_line - 1, m_column - 1); + sendMessage(lsp::CompletionRequest({doc, pos})); +} + +void LspClient::handleCompletionResponse() +{ + const lsp::CompletionResult result(lsp::CompletionRequest::Response(m_messageObject) + .result() + .value_or(lsp::CompletionResult())); + if (const auto items = std::get_if<QList<lsp::CompletionItem>>(&result)) { + for (const lsp::CompletionItem &item : *items) { + std::cout << qPrintable(item.label()); + if (item.detail()) + std::cout << ' ' << qPrintable(*item.detail()); + std::cout << std::endl; + } + } + exit(EXIT_SUCCESS); +} + lsp::DocumentUri LspClient::uri() const { return lsp::DocumentUri::fromFilePath(lsp::Utils::FilePath::fromUserInput(m_filePath), |