// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qqmllscompletion_p.h" using namespace QLspSpecification; using namespace QQmlJS::Dom; using namespace Qt::StringLiterals; QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(QQmlLSCompletionLog, "qt.languageserver.completions") /*! \class QQmlLSCompletion \internal \brief QQmlLSCompletion provides completions for all kinds of QML and JS constructs. Use the \l{completions} method to obtain completions at a certain DomItem. All the other methods in this class are helper methods: some compute completions for specific QML and JS constructs and some are shared between multiple QML or JS constructs to avoid code duplication. Most of the helper methods add their completion items via a BackInsertIterator. Some helper methods are called "suggest*" and will try to suggest code that does not exist yet. For example, any JS statement can be expected inside a Blockstatement so suggestJSStatementCompletion() is used to suggest JS statements inside of BlockStatements. Another example might be suggestReachableTypes() that will suggest Types for type annotations, attached types or Qml Object hierarchies, or suggestCaseAndDefaultStatementCompletion() that will only suggest "case" and "default" clauses for switch statements. Some helper methods are called "inside*" and will try to suggest code inside an existing structure. For example, insideForStatementCompletion() will try to suggest completion for the different code pieces initializer, condition, increment and statement that exist inside of: \badcode for(initializer; condition; increment) statement \endcode */ CompletionItem QQmlLSCompletion::makeSnippet(QUtf8StringView qualifier, QUtf8StringView label, QUtf8StringView insertText) { CompletionItem res; if (!qualifier.isEmpty()) { res.label = qualifier.data(); res.label += '.'; } res.label += label.data(); res.insertTextFormat = InsertTextFormat::Snippet; if (!qualifier.isEmpty()) { res.insertText = qualifier.data(); *res.insertText += '.'; *res.insertText += insertText.data(); } else { res.insertText = insertText.data(); } res.kind = int(CompletionItemKind::Snippet); res.insertTextMode = InsertTextMode::AdjustIndentation; return res; } CompletionItem QQmlLSCompletion::makeSnippet(QUtf8StringView label, QUtf8StringView insertText) { return makeSnippet(QByteArray(), label, insertText); } /*! \internal \brief Compare left and right locations to the position denoted by ctx, see special cases below. Statements and expressions need to provide different completions depending on where the cursor is. For example, lets take following for-statement: \badcode for (let i = 0; ; ++i) {} \endcode We want to provide script expression completion (method names, property names, available JS variables names, QML objects ids, and so on) at the place denoted by \c{}. The question is: how do we know that the cursor is really at \c{}? In the case of the for-loop, we can compare the position of the cursor with the first and the second semicolon of the for loop. If the first semicolon does not exist, it has an invalid sourcelocation and the cursor is definitively \e{not} at \c{}. Therefore, return false when \c{left} is invalid. If the second semicolon does not exist, then just ignore it: it might not have been written yet. */ bool QQmlLSCompletion::betweenLocations(QQmlJS::SourceLocation left, const QQmlLSCompletionPosition &positionInfo, QQmlJS::SourceLocation right) const { if (!left.isValid()) return false; // note: left.end() == ctx.offset() means that the cursor lies exactly after left if (!(left.end() <= positionInfo.offset())) return false; if (!right.isValid()) return true; // note: ctx.offset() == right.begin() means that the cursor lies exactly before right return positionInfo.offset() <= right.begin(); } /*! \internal Returns true if ctx denotes an offset lying behind left.end(), and false otherwise. */ bool QQmlLSCompletion::afterLocation(QQmlJS::SourceLocation left, const QQmlLSCompletionPosition &positionInfo) const { return betweenLocations(left, positionInfo, QQmlJS::SourceLocation{}); } /*! \internal Returns true if ctx denotes an offset lying before right.begin(), and false otherwise. */ bool QQmlLSCompletion::beforeLocation(const QQmlLSCompletionPosition &ctx, QQmlJS::SourceLocation right) const { if (!right.isValid()) return true; // note: ctx.offset() == right.begin() means that the cursor lies exactly before right if (ctx.offset() <= right.begin()) return true; return false; } bool QQmlLSCompletion::ctxBeforeStatement(const QQmlLSCompletionPosition &positionInfo, const DomItem &parentForContext, FileLocationRegion firstRegion) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const bool result = beforeLocation(positionInfo, regions[firstRegion]); return result; } void QQmlLSCompletion::suggestBindingCompletion(const DomItem &itemAtPosition, BackInsertIterator it) const { suggestReachableTypes(itemAtPosition, LocalSymbolsType::AttachedType, CompletionItemKind::Class, it); const QQmlJSScope::ConstPtr scope = [&]() { if (!QQmlLSUtils::isFieldMemberAccess(itemAtPosition)) return itemAtPosition.qmlObject().semanticScope(); const DomItem owner = itemAtPosition.directParent().field(Fields::left); auto expressionType = QQmlLSUtils::resolveExpressionType( owner, ResolveActualTypeForFieldMemberExpression); return expressionType ? expressionType->semanticScope : QQmlJSScope::ConstPtr{}; }(); if (!scope) return; propertyCompletion(scope, nullptr, it); signalHandlerCompletion(scope, nullptr, it); } void QQmlLSCompletion::insideImportCompletionHelper(const DomItem &file, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator it) const { // returns completions for import statements, ctx is supposed to be in an import statement const CompletionContextStrings &ctx = positionInfo.cursorPosition; ImportCompletionType importCompletionType = ImportCompletionType::None; QRegularExpression spaceRe(uR"(\W+)"_s); QList linePieces = ctx.preLine().split(spaceRe, Qt::SkipEmptyParts); qsizetype effectiveLength = linePieces.size() + ((!ctx.preLine().isEmpty() && ctx.preLine().last().isSpace()) ? 1 : 0); if (effectiveLength < 2) { CompletionItem comp; comp.label = "import"; comp.kind = int(CompletionItemKind::Keyword); it = comp; } if (linePieces.isEmpty() || linePieces.first() != u"import") return; if (effectiveLength == 2) { // the cursor is after the import, possibly in a partial module name importCompletionType = ImportCompletionType::Module; } else if (effectiveLength == 3) { if (linePieces.last() != u"as") { // the cursor is after the module, possibly in a partial version token (or partial as) CompletionItem comp; comp.label = "as"; comp.kind = int(CompletionItemKind::Keyword); it = comp; importCompletionType = ImportCompletionType::Version; } } DomItem env = file.environment(); if (std::shared_ptr envPtr = env.ownerAs()) { switch (importCompletionType) { case ImportCompletionType::None: break; case ImportCompletionType::Module: { QDuplicateTracker modulesSeen; for (const QString &uri : envPtr->moduleIndexUris(env)) { QStringView base = ctx.base(); // if we allow spaces we should get rid of them if (uri.startsWith(base)) { QStringList rest = uri.mid(base.size()).split(u'.'); if (rest.isEmpty()) continue; const QString label = rest.first(); if (!modulesSeen.hasSeen(label)) { CompletionItem comp; comp.label = label.toUtf8(); comp.kind = int(CompletionItemKind::Module); it = comp; } } } break; } case ImportCompletionType::Version: if (ctx.base().isEmpty()) { for (int majorV : envPtr->moduleIndexMajorVersions(env, linePieces.at(1).toString())) { CompletionItem comp; comp.label = QString::number(majorV).toUtf8(); comp.kind = int(CompletionItemKind::Constant); it = comp; } } else { bool hasMajorVersion = ctx.base().endsWith(u'.'); int majorV = -1; if (hasMajorVersion) majorV = ctx.base().mid(0, ctx.base().size() - 1).toInt(&hasMajorVersion); if (!hasMajorVersion) break; if (std::shared_ptr mIndex = envPtr->moduleIndexWithUri(env, linePieces.at(1).toString(), majorV)) { for (int minorV : mIndex->minorVersions()) { CompletionItem comp; comp.label = QString::number(minorV).toUtf8(); comp.kind = int(CompletionItemKind::Constant); it = comp; } } } break; } } } void QQmlLSCompletion::idsCompletions(const DomItem &component, BackInsertIterator it) const { qCDebug(QQmlLSCompletionLog) << "adding ids completions"; for (const QString &k : component.field(Fields::ids).keys()) { CompletionItem comp; comp.label = k.toUtf8(); comp.kind = int(CompletionItemKind::Value); it = comp; } } static bool testScopeSymbol(const QQmlJSScope::ConstPtr &scope, LocalSymbolsTypes options, CompletionItemKind kind) { const bool currentIsSingleton = scope->isSingleton(); const bool currentIsAttached = !scope->attachedType().isNull(); if ((options & LocalSymbolsType::Singleton) && currentIsSingleton) { return true; } if ((options & LocalSymbolsType::AttachedType) && currentIsAttached) { return true; } const bool isObjectType = scope->isReferenceType(); if (options & LocalSymbolsType::ObjectType && !currentIsSingleton && isObjectType) { return kind != CompletionItemKind::Constructor || scope->isCreatable(); } if (options & LocalSymbolsType::ValueType && !currentIsSingleton && !isObjectType) { return true; } return false; } /*! \internal Obtain the types reachable from \c{el} as a CompletionItems. */ void QQmlLSCompletion::suggestReachableTypes(const DomItem &el, LocalSymbolsTypes options, CompletionItemKind kind, BackInsertIterator it) const { auto file = el.containingFile().as(); if (!file) return; auto resolver = file->typeResolver(); if (!resolver) return; const QString requiredQualifiers = QQmlLSUtils::qualifiersFrom(el); const auto keyValueRange = resolver->importedTypes().asKeyValueRange(); for (const auto &type : keyValueRange) { // ignore special QQmlJSImporterMarkers const bool isMarkerType = type.first.contains(u"$internal$.") || type.first.contains(u"$anonymous$.") || type.first.contains(u"$module$."); if (isMarkerType || !type.first.startsWith(requiredQualifiers)) continue; auto &scope = type.second.scope; if (!scope) continue; if (!testScopeSymbol(scope, options, kind)) continue; CompletionItem completion; completion.label = QStringView(type.first).sliced(requiredQualifiers.size()).toUtf8(); completion.kind = int(kind); it = completion; } } void QQmlLSCompletion::jsIdentifierCompletion(const QQmlJSScope::ConstPtr &scope, QDuplicateTracker *usedNames, BackInsertIterator it) const { for (const auto &[name, jsIdentifier] : scope->ownJSIdentifiers().asKeyValueRange()) { CompletionItem completion; if (usedNames && usedNames->hasSeen(name)) { continue; } completion.label = name.toUtf8(); completion.kind = int(CompletionItemKind::Variable); QString detail = u"has type "_s; if (jsIdentifier.typeName) { if (jsIdentifier.isConst) { detail.append(u"const "); } detail.append(*jsIdentifier.typeName); } else { detail.append(jsIdentifier.isConst ? u"const"_s : u"var"_s); } completion.detail = detail.toUtf8(); it = completion; } } void QQmlLSCompletion::methodCompletion(const QQmlJSScope::ConstPtr &scope, QDuplicateTracker *usedNames, BackInsertIterator it) const { // JS functions in current and base scopes for (const auto &[name, method] : scope->methods().asKeyValueRange()) { if (method.access() != QQmlJSMetaMethod::Public) continue; if (usedNames && usedNames->hasSeen(name)) { continue; } CompletionItem completion; completion.label = name.toUtf8(); completion.kind = int(CompletionItemKind::Method); it = completion; // TODO: QQmlLSUtils::reachableSymbols seems to be able to do documentation and detail // and co, it should also be done here if possible. } } void QQmlLSCompletion::propertyCompletion(const QQmlJSScope::ConstPtr &scope, QDuplicateTracker *usedNames, BackInsertIterator it) const { for (const auto &[name, property] : scope->properties().asKeyValueRange()) { if (usedNames && usedNames->hasSeen(name)) { continue; } CompletionItem completion; completion.label = name.toUtf8(); completion.kind = int(CompletionItemKind::Property); QString detail{ u"has type "_s }; if (!property.isWritable()) detail.append(u"readonly "_s); detail.append(property.typeName().isEmpty() ? u"var"_s : property.typeName()); completion.detail = detail.toUtf8(); it = completion; } } void QQmlLSCompletion::enumerationCompletion(const QQmlJSScope::ConstPtr &scope, QDuplicateTracker *usedNames, BackInsertIterator it) const { for (const QQmlJSMetaEnum &enumerator : scope->enumerations()) { if (usedNames && usedNames->hasSeen(enumerator.name())) { continue; } CompletionItem completion; completion.label = enumerator.name().toUtf8(); completion.kind = static_cast(CompletionItemKind::Enum); it = completion; } } void QQmlLSCompletion::enumerationValueCompletionHelper(const QStringList &enumeratorKeys, BackInsertIterator it) const { for (const QString &enumeratorKey : enumeratorKeys) { CompletionItem completion; completion.label = enumeratorKey.toUtf8(); completion.kind = static_cast(CompletionItemKind::EnumMember); it = completion; } } /*! \internal Creates completion items for enumerationvalues. If enumeratorName is a valid enumerator then only do completion for the requested enumerator, and otherwise do completion for \b{all other possible} enumerators. For example: ``` id: someItem enum Hello { World } enum MyEnum { ValueOne, ValueTwo } // Hello does refer to a enumerator: property var a: Hello. // someItem does not refer to a enumerator: property var b: someItem. ``` */ void QQmlLSCompletion::enumerationValueCompletion(const QQmlJSScope::ConstPtr &scope, const QString &enumeratorName, BackInsertIterator result) const { auto enumerator = scope->enumeration(enumeratorName); if (enumerator.isValid()) { enumerationValueCompletionHelper(enumerator.keys(), result); return; } for (const QQmlJSMetaEnum &enumerator : scope->enumerations()) { enumerationValueCompletionHelper(enumerator.keys(), result); } } /*! \internal Calls F on all JavaScript-parents of scope. For example, you can use this method to collect all the JavaScript Identifiers from following code: ``` { // this block statement contains only 'x' let x = 3; { // this block statement contains only 'y', and 'x' has to be retrieved via its parent. let y = 4; } } ``` */ template void collectFromAllJavaScriptParents(const F &&f, const QQmlJSScope::ConstPtr &scope) { for (QQmlJSScope::ConstPtr current = scope; current; current = current->parentScope()) { f(current); if (current->scopeType() == QQmlSA::ScopeType::QMLScope) return; } } /*! \internal Generate autocompletions for JS expressions, suggest possible properties, methods, etc. If scriptIdentifier is inside a Field Member Expression, like \c{onCompleted} in \c{Component.onCompleted} for example, then this method will only suggest properties, methods, etc from the correct type. For the previous example that would be properties, methods, etc. from the Component attached type. */ void QQmlLSCompletion::suggestJSExpressionCompletion(const DomItem &scriptIdentifier, BackInsertIterator result) const { QDuplicateTracker usedNames; QQmlJSScope::ConstPtr nearestScope; // note: there is an edge case, where the user asks for completion right after the dot // of some qualified expression like `root.hello`. In this case, scriptIdentifier is actually // the BinaryExpression instead of the left-hand-side that has not be written down yet. const bool askForCompletionOnDot = QQmlLSUtils::isFieldMemberExpression(scriptIdentifier); const bool hasQualifier = QQmlLSUtils::isFieldMemberAccess(scriptIdentifier) || askForCompletionOnDot; if (!hasQualifier) { for (QUtf8StringView view : std::array{ "null", "false", "true" }) { CompletionItem completion; completion.label = view.data(); completion.kind = int(CompletionItemKind::Value); result = completion; } idsCompletions(scriptIdentifier.component(), result); suggestReachableTypes(scriptIdentifier, LocalSymbolsType::Singleton | LocalSymbolsType::AttachedType, CompletionItemKind::Class, result); auto scope = scriptIdentifier.nearestSemanticScope(); if (!scope) return; nearestScope = scope; enumerationCompletion(nearestScope, &usedNames, result); } else { const DomItem owner = (askForCompletionOnDot ? scriptIdentifier : scriptIdentifier.directParent()) .field(Fields::left); auto expressionType = QQmlLSUtils::resolveExpressionType( owner, ResolveActualTypeForFieldMemberExpression); if (!expressionType || !expressionType->semanticScope) return; nearestScope = expressionType->semanticScope; // Use root element scope to use find the enumerations // This should be changed when we support usages in external files if (expressionType->type == QmlComponentIdentifier) nearestScope = owner.rootQmlObject(GoTo::MostLikely).semanticScope(); if (expressionType->name) { // note: you only get enumeration values in qualified expressions, never alone enumerationValueCompletion(nearestScope, *expressionType->name, result); // skip enumeration types if already inside an enumeration type if (auto enumerator = nearestScope->enumeration(*expressionType->name); !enumerator.isValid()) { enumerationCompletion(nearestScope, &usedNames, result); } if (expressionType->type == EnumeratorIdentifier) return; } } Q_ASSERT(nearestScope); methodCompletion(nearestScope, &usedNames, result); propertyCompletion(nearestScope, &usedNames, result); if (!hasQualifier) { // collect all of the stuff from parents collectFromAllJavaScriptParents( [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) { jsIdentifierCompletion(scope, &usedNames, result); }, nearestScope); collectFromAllJavaScriptParents( [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) { methodCompletion(scope, &usedNames, result); }, nearestScope); collectFromAllJavaScriptParents( [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) { propertyCompletion(scope, &usedNames, result); }, nearestScope); auto file = scriptIdentifier.containingFile().as(); if (!file) return; auto resolver = file->typeResolver(); if (!resolver) return; const auto globals = resolver->jsGlobalObject(); methodCompletion(globals, &usedNames, result); propertyCompletion(globals, &usedNames, result); } } static const QQmlJSScope *resolve(const QQmlJSScope *current, const QStringList &names) { for (const QString &name : names) { if (auto property = current->property(name); property.isValid()) { if (auto propertyType = property.type().get()) { current = propertyType; continue; } } return {}; } return current; } bool QQmlLSCompletion::cursorInFrontOfItem(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo) { auto fileLocations = FileLocations::treeOf(parentForContext)->info().fullRegion; return positionInfo.offset() <= fileLocations.offset; } bool QQmlLSCompletion::cursorAfterColon(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo) { auto location = FileLocations::treeOf(currentItem)->info(); auto region = location.regions.constFind(ColonTokenRegion); if (region == location.regions.constEnd()) return false; if (region.value().isValid() && region.value().offset < positionInfo.offset()) { return true; } return false; } /*! \internal \brief Mapping from pragma names to allowed pragma values. This mapping of pragma names to pragma values is not complete. In fact, it only contains the pragma names and values that one should see autocompletion for. Some pragmas like FunctionSignatureBehavior or Strict or the Reference/Value of ValueTypeBehavior, for example, should currently not be proposed as completion items by qmlls. An empty QList-value in the QMap means that the pragma does not accept pragma values. */ static const QMap> valuesForPragmas{ { u"ComponentBehavior"_s, { u"Unbound"_s, u"Bound"_s } }, { u"NativeMethodBehavior"_s, { u"AcceptThisObject"_s, u"RejectThisObject"_s } }, { u"ListPropertyAssignBehavior"_s, { u"Append"_s, u"Replace"_s, u"ReplaceIfNotDefault"_s } }, { u"Singleton"_s, {} }, { u"ValueTypeBehavior"_s, { u"Addressable"_s, u"Inaddressable"_s } }, }; void QQmlLSCompletion::insidePragmaCompletion(QQmlJS::Dom::DomItem currentItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { if (cursorAfterColon(currentItem, positionInfo)) { const QString name = currentItem.field(Fields::name).value().toString(); auto values = valuesForPragmas.constFind(name); if (values == valuesForPragmas.constEnd()) return; for (const auto &value : *values) { CompletionItem comp; comp.label = value.toUtf8(); comp.kind = static_cast(CompletionItemKind::Value); result = comp; } return; } for (const auto &pragma : valuesForPragmas.asKeyValueRange()) { CompletionItem comp; comp.label = pragma.first.toUtf8(); if (!pragma.second.isEmpty()) { comp.insertText = QString(pragma.first).append(u": ").toUtf8(); } comp.kind = static_cast(CompletionItemKind::Value); result = comp; } } void QQmlLSCompletion::insideQmlObjectCompletion(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation leftBrace = regions[LeftBraceRegion]; const QQmlJS::SourceLocation rightBrace = regions[RightBraceRegion]; if (beforeLocation(positionInfo, leftBrace)) { LocalSymbolsTypes options; options.setFlag(LocalSymbolsType::ObjectType); suggestReachableTypes(positionInfo.itemAtPosition, options, CompletionItemKind::Constructor, result); suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result); if (QQmlLSUtils::isFieldMemberExpression(positionInfo.itemAtPosition)) { /*! \internal In the case that a missing identifier is followed by an assignment to the default property, the parser will create a QmlObject out of both binding and default binding. For example, in \code property int x: root. Item {} \endcode the parser will create one binding containing one QmlObject of type `root.Item`, instead of two bindings (one for `x` and one for the default property). For this special case, if completion is requested inside `root.Item`, then try to also suggest JS expressions. Note: suggestJSExpressionCompletion() will suggest nothing if the fieldMemberExpression starts with the name of a qualified module or a filename, so this only adds invalid suggestions in the case that there is something shadowing the qualified module name or filename, like a property name for example. Note 2: This does not happen for field member accesses. For example, in \code property int x: root.x Item {} \endcode The parser will create both bindings correctly. */ suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); } return; } if (betweenLocations(leftBrace, positionInfo, rightBrace)) { // default/required property completion for (QUtf8StringView view : std::array{ "", "readonly ", "default ", "default required ", "required default ", "required " }) { // readonly properties require an initializer if (view != QUtf8StringView("readonly ")) { result = makeSnippet( QByteArray(view.data()).append("property type name;"), QByteArray(view.data()).append("property ${1:type} ${0:name};")); } result = makeSnippet( QByteArray(view.data()).append("property type name: value;"), QByteArray(view.data()).append("property ${1:type} ${2:name}: ${0:value};")); } // signal result = makeSnippet("signal name(arg1:type1, ...)", "signal ${1:name}($0)"); // signal without parameters result = makeSnippet("signal name;", "signal ${0:name};"); // make already existing property required result = makeSnippet("required name;", "required ${0:name};"); // function result = makeSnippet("function name(args...): returnType { statements...}", "function ${1:name}($2): ${3:returnType} {\n\t$0\n}"); // enum result = makeSnippet("enum name { Values...}", "enum ${1:name} {\n\t${0:values}\n}"); // inline component result = makeSnippet("component Name: BaseType { ... }", "component ${1:name}: ${2:baseType} {\n\t$0\n}"); // add bindings const DomItem containingObject = parentForContext.qmlObject(); suggestBindingCompletion(containingObject, result); // add Qml Types for default binding const DomItem containingFile = parentForContext.containingFile(); suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType, CompletionItemKind::Constructor, result); suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insidePropertyDefinitionCompletion( const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { auto info = FileLocations::treeOf(currentItem)->info(); const QQmlJS::SourceLocation propertyKeyword = info.regions[PropertyKeywordRegion]; // do completions for the keywords if (positionInfo.offset() < propertyKeyword.offset + propertyKeyword.length) { const QQmlJS::SourceLocation readonlyKeyword = info.regions[ReadonlyKeywordRegion]; const QQmlJS::SourceLocation defaultKeyword = info.regions[DefaultKeywordRegion]; const QQmlJS::SourceLocation requiredKeyword = info.regions[RequiredKeywordRegion]; bool completeReadonly = true; bool completeRequired = true; bool completeDefault = true; // if there is already a readonly keyword before the cursor: do not auto complete it again if (readonlyKeyword.isValid() && readonlyKeyword.offset < positionInfo.offset()) { completeReadonly = false; // also, required keywords do not like readonly keywords completeRequired = false; } // same for required if (requiredKeyword.isValid() && requiredKeyword.offset < positionInfo.offset()) { completeRequired = false; // also, required keywords do not like readonly keywords completeReadonly = false; } // same for default if (defaultKeyword.isValid() && defaultKeyword.offset < positionInfo.offset()) { completeDefault = false; } auto addCompletionKeyword = [&result](QUtf8StringView view, bool complete) { if (!complete) return; CompletionItem item; item.label = view.data(); item.kind = int(CompletionItemKind::Keyword); result = item; }; addCompletionKeyword(u8"readonly", completeReadonly); addCompletionKeyword(u8"required", completeRequired); addCompletionKeyword(u8"default", completeDefault); addCompletionKeyword(u8"property", true); return; } const QQmlJS::SourceLocation propertyIdentifier = info.regions[IdentifierRegion]; if (propertyKeyword.end() <= positionInfo.offset() && positionInfo.offset() < propertyIdentifier.offset) { suggestReachableTypes(currentItem, LocalSymbolsType::ObjectType | LocalSymbolsType::ValueType, CompletionItemKind::Class, result); } // do not autocomplete the rest return; } void QQmlLSCompletion::insideBindingCompletion(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const DomItem containingBinding = currentItem.filterUp( [](DomType type, const QQmlJS::Dom::DomItem &) { return type == DomType::Binding; }, FilterUpOptions::ReturnOuter); // do scriptidentifiercompletion after the ':' of a binding if (cursorAfterColon(containingBinding, positionInfo)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); if (auto type = QQmlLSUtils::resolveExpressionType(currentItem, ResolveOwnerType)) { const QStringList names = currentItem.field(Fields::name).toString().split(u'.'); const QQmlJSScope *current = resolve(type->semanticScope.get(), names); // add type names when binding to an object type or a property with var type if (!current || current->accessSemantics() == QQmlSA::AccessSemantics::Reference) { LocalSymbolsTypes options; options.setFlag(LocalSymbolsType::ObjectType); suggestReachableTypes(positionInfo.itemAtPosition, options, CompletionItemKind::Constructor, result); suggestSnippetsForRightHandSideOfBinding(positionInfo.itemAtPosition, result); } } return; } // ignore the binding if asking for completion in front of the binding if (cursorInFrontOfItem(containingBinding, positionInfo)) { insideQmlObjectCompletion(currentItem.containingObject(), positionInfo, result); return; } const DomItem containingObject = currentItem.qmlObject(); suggestBindingCompletion(positionInfo.itemAtPosition, result); // add Qml Types for default binding suggestReachableTypes(positionInfo.itemAtPosition, LocalSymbolsType::ObjectType, CompletionItemKind::Constructor, result); suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result); } void QQmlLSCompletion::insideImportCompletion(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const DomItem containingFile = currentItem.containingFile(); insideImportCompletionHelper(containingFile, positionInfo, result); // when in front of the import statement: propose types for root Qml Object completion if (cursorInFrontOfItem(currentItem, positionInfo)) { suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType, CompletionItemKind::Constructor, result); } } void QQmlLSCompletion::insideQmlFileCompletion(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const DomItem containingFile = currentItem.containingFile(); // completions for code outside the root Qml Object // global completions if (positionInfo.cursorPosition.atLineStart()) { if (positionInfo.cursorPosition.base().isEmpty()) { for (const QStringView &s : std::array({ u"pragma", u"import" })) { CompletionItem comp; comp.label = s.toUtf8(); comp.kind = int(CompletionItemKind::Keyword); result = comp; } } } // Types for root Qml Object completion suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType, CompletionItemKind::Constructor, result); } /*! \internal Generate the snippets for let, var and const variable declarations. */ void QQmlLSCompletion::suggestVariableDeclarationStatementCompletion( BackInsertIterator result, QQmlLSUtilsAppendOption option) const { // let/var/const statement for (auto view : std::array{ "let", "var", "const" }) { auto snippet = makeSnippet(QByteArray(view.data()).append(" variable = value"), QByteArray(view.data()).append(" ${1:variable} = $0")); if (option == AppendSemicolon) { snippet.insertText->append(";"); snippet.label.append(";"); } result = snippet; } } /*! \internal Generate the snippets for case and default statements. */ void QQmlLSCompletion::suggestCaseAndDefaultStatementCompletion(BackInsertIterator result) const { // case snippet result = makeSnippet("case value: statements...", "case ${1:value}:\n\t$0"); // case + brackets snippet result = makeSnippet("case value: { statements... }", "case ${1:value}: {\n\t$0\n}"); // default snippet result = makeSnippet("default: statements...", "default:\n\t$0"); // default + brackets snippet result = makeSnippet("default: { statements... }", "default: {\n\t$0\n}"); } /*! \internal Break and continue can be inserted only in following situations: \list \li Break and continue inside a loop. \li Break inside a (nested) LabelledStatement \li Break inside a (nested) SwitchStatement \endlist */ void QQmlLSCompletion::suggestContinueAndBreakStatementIfNeeded(const DomItem &itemAtPosition, BackInsertIterator result) const { bool alreadyInLabel = false; bool alreadyInSwitch = false; for (DomItem current = itemAtPosition; current; current = current.directParent()) { switch (current.internalKind()) { case DomType::ScriptExpression: // reached end of script expression return; case DomType::ScriptForStatement: case DomType::ScriptForEachStatement: case DomType::ScriptWhileStatement: case DomType::ScriptDoWhileStatement: { CompletionItem continueKeyword; continueKeyword.label = "continue"; continueKeyword.kind = int(CompletionItemKind::Keyword); result = continueKeyword; // do not add break twice if (!alreadyInSwitch && !alreadyInLabel) { CompletionItem breakKeyword; breakKeyword.label = "break"; breakKeyword.kind = int(CompletionItemKind::Keyword); result = breakKeyword; } // early exit: cannot suggest more completions return; } case DomType::ScriptSwitchStatement: { // check if break was already inserted if (alreadyInSwitch || alreadyInLabel) break; alreadyInSwitch = true; CompletionItem breakKeyword; breakKeyword.label = "break"; breakKeyword.kind = int(CompletionItemKind::Keyword); result = breakKeyword; break; } case DomType::ScriptLabelledStatement: { // check if break was already inserted because of switch or loop if (alreadyInSwitch || alreadyInLabel) break; alreadyInLabel = true; CompletionItem breakKeyword; breakKeyword.label = "break"; breakKeyword.kind = int(CompletionItemKind::Keyword); result = breakKeyword; break; } default: break; } } } /*! \internal Generates snippets or keywords for all possible JS statements where it makes sense. To use whenever any JS statement can be expected, but when no JS statement is there yet. Only generates JS expression completions when itemAtPosition is a qualified name. Here is a list of statements that do \e{not} get any snippets: \list \li BlockStatement does not need a code snippet, editors automatically include the closing bracket anyway. \li EmptyStatement completion would only generate a single \c{;} \li ExpressionStatement completion cannot generate any snippet, only identifiers \li WithStatement completion is not recommended: qmllint will warn about usage of with statements \li LabelledStatement completion might need to propose labels (TODO?) \li DebuggerStatement completion does not strike as being very useful \endlist */ void QQmlLSCompletion::suggestJSStatementCompletion(const DomItem &itemAtPosition, BackInsertIterator result) const { suggestJSExpressionCompletion(itemAtPosition, result); if (QQmlLSUtils::isFieldMemberAccess(itemAtPosition)) return; // expression statements suggestVariableDeclarationStatementCompletion(result); // block statement result = makeSnippet("{ statements... }", "{\n\t$0\n}"); // if + brackets statement result = makeSnippet("if (condition) { statements }", "if ($1) {\n\t$0\n}"); // do statement result = makeSnippet("do { statements } while (condition);", "do {\n\t$1\n} while ($0);"); // while + brackets statement result = makeSnippet("while (condition) { statements...}", "while ($1) {\n\t$0\n}"); // for + brackets loop statement result = makeSnippet("for (initializer; condition; increment) { statements... }", "for ($1;$2;$3) {\n\t$0\n}"); // for ... in + brackets loop statement result = makeSnippet("for (property in object) { statements... }", "for ($1 in $2) {\n\t$0\n}"); // for ... of + brackets loop statement result = makeSnippet("for (element of array) { statements... }", "for ($1 of $2) {\n\t$0\n}"); // try + catch statement result = makeSnippet("try { statements... } catch(error) { statements... }", "try {\n\t$1\n} catch($2) {\n\t$0\n}"); // try + finally statement result = makeSnippet("try { statements... } finally { statements... }", "try {\n\t$1\n} finally {\n\t$0\n}"); // try + catch + finally statement result = makeSnippet( "try { statements... } catch(error) { statements... } finally { statements... }", "try {\n\t$1\n} catch($2) {\n\t$3\n} finally {\n\t$0\n}"); // one can always assume that JS code in QML is inside a function, so always propose `return` for (auto &&view : { "return"_ba, "throw"_ba }) { CompletionItem item; item.label = std::move(view); item.kind = int(CompletionItemKind::Keyword); result = item; } // rules for case+default statements: // 1) when inside a CaseBlock, or // 2) inside a CaseClause, as an (non-nested) element of the CaseClause statementlist. // 3) inside a DefaultClause, as an (non-nested) element of the DefaultClause statementlist, // // switch (x) { // // (1) // case 1: // myProperty = 5; // // (2) -> could be another statement of current case, but also a new case or default! // default: // myProperty = 5; // // (3) -> could be another statement of current default, but also a new case or default! // } const DomType currentKind = itemAtPosition.internalKind(); const DomType parentKind = itemAtPosition.directParent().internalKind(); if (currentKind == DomType::ScriptCaseBlock || currentKind == DomType::ScriptCaseClause || currentKind == DomType::ScriptDefaultClause || (currentKind == DomType::List && (parentKind == DomType::ScriptCaseClause || parentKind == DomType::ScriptDefaultClause))) { suggestCaseAndDefaultStatementCompletion(result); } suggestContinueAndBreakStatementIfNeeded(itemAtPosition, result); } void QQmlLSCompletion::insideForStatementCompletion(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; const QQmlJS::SourceLocation firstSemicolon = regions[FirstSemicolonTokenRegion]; const QQmlJS::SourceLocation secondSemicolon = regions[SecondSemicolonRegion]; const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; if (betweenLocations(leftParenthesis, positionInfo, firstSemicolon)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); suggestVariableDeclarationStatementCompletion(result, QQmlLSUtilsAppendOption::AppendNothing); return; } if (betweenLocations(firstSemicolon, positionInfo, secondSemicolon) || betweenLocations(secondSemicolon, positionInfo, rightParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (afterLocation(rightParenthesis, positionInfo)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideScriptLiteralCompletion(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { Q_UNUSED(currentItem); if (positionInfo.cursorPosition.base().isEmpty()) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideCallExpression(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(currentItem)->info().regions; const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; if (beforeLocation(positionInfo, leftParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideIfStatement(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(currentItem)->info().regions; const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; const QQmlJS::SourceLocation elseKeyword = regions[ElseKeywordRegion]; if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (betweenLocations(rightParenthesis, positionInfo, elseKeyword)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; } if (afterLocation(elseKeyword, positionInfo)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideReturnStatement(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(currentItem)->info().regions; const QQmlJS::SourceLocation returnKeyword = regions[ReturnKeywordRegion]; if (afterLocation(returnKeyword, positionInfo)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideWhileStatement(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(currentItem)->info().regions; const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (afterLocation(rightParenthesis, positionInfo)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideDoWhileStatement(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation doKeyword = regions[DoKeywordRegion]; const QQmlJS::SourceLocation whileKeyword = regions[WhileKeywordRegion]; const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; if (betweenLocations(doKeyword, positionInfo, whileKeyword)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; } if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideForEachStatement(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation inOf = regions[InOfTokenRegion]; const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; if (betweenLocations(leftParenthesis, positionInfo, inOf)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); suggestVariableDeclarationStatementCompletion(result); return; } if (betweenLocations(inOf, positionInfo, rightParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (afterLocation(rightParenthesis, positionInfo)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideSwitchStatement(const DomItem &parentForContext, const QQmlLSCompletionPosition positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideCaseClause(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation caseKeyword = regions[CaseKeywordRegion]; const QQmlJS::SourceLocation colonToken = regions[ColonTokenRegion]; if (betweenLocations(caseKeyword, positionInfo, colonToken)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (afterLocation(colonToken, positionInfo)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; } } /*! \internal Checks if a case or default clause does happen before ctx in the code. */ bool QQmlLSCompletion::isCaseOrDefaultBeforeCtx(const DomItem ¤tClause, const QQmlLSCompletionPosition &positionInfo, FileLocationRegion keywordRegion) const { Q_ASSERT(keywordRegion == QQmlJS::Dom::CaseKeywordRegion || keywordRegion == QQmlJS::Dom::DefaultKeywordRegion); if (!currentClause) return false; const auto token = FileLocations::treeOf(currentClause)->info().regions[keywordRegion]; if (afterLocation(token, positionInfo)) return true; return false; } /*! \internal Search for a `case ...:` or a `default: ` clause happening before ctx, and return the corresponding DomItem of type DomType::CaseClauses or DomType::DefaultClause. Return an empty DomItem if neither case nor default was found. */ DomItem QQmlLSCompletion::previousCaseOfCaseBlock(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo) const { const DomItem caseClauses = parentForContext.field(Fields::caseClauses); for (int i = 0; i < caseClauses.indexes(); ++i) { const DomItem currentClause = caseClauses.index(i); if (isCaseOrDefaultBeforeCtx(currentClause, positionInfo, QQmlJS::Dom::CaseKeywordRegion)) { return currentClause; } } const DomItem defaultClause = parentForContext.field(Fields::defaultClause); if (isCaseOrDefaultBeforeCtx(defaultClause, positionInfo, QQmlJS::Dom::DefaultKeywordRegion)) return parentForContext.field(Fields::defaultClause); const DomItem moreCaseClauses = parentForContext.field(Fields::moreCaseClauses); for (int i = 0; i < moreCaseClauses.indexes(); ++i) { const DomItem currentClause = moreCaseClauses.index(i); if (isCaseOrDefaultBeforeCtx(currentClause, positionInfo, QQmlJS::Dom::CaseKeywordRegion)) { return currentClause; } } return {}; } void QQmlLSCompletion::insideCaseBlock(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation leftBrace = regions[LeftBraceRegion]; const QQmlJS::SourceLocation rightBrace = regions[RightBraceRegion]; if (!betweenLocations(leftBrace, positionInfo, rightBrace)) return; // TODO: looks fishy // if there is a previous case or default clause, you can still add statements to it if (const auto previousCase = previousCaseOfCaseBlock(parentForContext, positionInfo)) { suggestJSStatementCompletion(previousCase, result); return; } // otherwise, only complete case and default suggestCaseAndDefaultStatementCompletion(result); } void QQmlLSCompletion::insideDefaultClause(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation colonToken = regions[ColonTokenRegion]; if (afterLocation(colonToken, positionInfo)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return ; } } void QQmlLSCompletion::insideBinaryExpressionCompletion( const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation operatorLocation = regions[OperatorTokenRegion]; if (beforeLocation(positionInfo, operatorLocation)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (afterLocation(operatorLocation, positionInfo)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } /*! \internal Doing completion in variable declarations requires taking a look at all different cases: \list \li Normal variable names, like \c{let helloWorld = 123;} In this case, only autocomplete scriptexpressionidentifiers after the '=' token. Do not propose existing names for the variable name, because the variable name needs to be an identifier that is not used anywhere (to avoid shadowing and confusing code), \li Deconstructed arrays, like \c{let [ helloWorld, ] = [ 123, ];} In this case, only autocomplete scriptexpressionidentifiers after the '=' token. Do not propose already existing identifiers inside the left hand side array. \li Deconstructed arrays with initializers, like \c{let [ helloWorld = someVar, ] = [ 123, ];} Note: this assigns the value of someVar to helloWorld if the right hand side's first element is undefined or does not exist. In this case, only autocomplete scriptexpressionidentifiers after the '=' tokens. Only propose already existing identifiers inside the left hand side array when behind a '=' token. \li Deconstructed Objects, like \c{let { helloWorld, } = { helloWorld: 123, };} In this case, only autocomplete scriptexpressionidentifiers after the '=' token. Do not propose already existing identifiers inside the left hand side object. \li Deconstructed Objects with initializers, like \c{let { helloWorld = someVar, } = {};} Note: this assigns the value of someVar to helloWorld if the right hand side's object does not have a property called 'helloWorld'. In this case, only autocomplete scriptexpressionidentifiers after the '=' token. Only propose already existing identifiers inside the left hand side object when behind a '=' token. \li Finally, you are allowed to nest and combine all above possibilities together for all your deconstruction needs, so the exact same completion needs to be done for DomType::ScriptPatternElement too. \endlist */ void QQmlLSCompletion::insideScriptPattern(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation equal = regions[EqualTokenRegion]; if (!afterLocation(equal, positionInfo)) return; // otherwise, only complete case and default suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); } /*! \internal See comment on insideScriptPattern(). */ void QQmlLSCompletion::insideVariableDeclarationEntry(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { insideScriptPattern(parentForContext, positionInfo, result); } void QQmlLSCompletion::insideThrowStatement(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation throwKeyword = regions[ThrowKeywordRegion]; if (afterLocation(throwKeyword, positionInfo)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideLabelledStatement(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation colon = regions[ColonTokenRegion]; if (afterLocation(colon, positionInfo)) { suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; } // note: the case "beforeLocation(ctx, colon)" probably never happens: // this is because without the colon, the parser will probably not parse this as a // labelledstatement but as a normal expression statement. // So this case only happens when the colon already exists, and the user goes back to the // label name and requests completion for that label. } /*! \internal Collect the current set of labels that some DomItem can jump to. */ static void collectLabels(const DomItem &context, QQmlLSCompletion::BackInsertIterator result) { for (DomItem current = context; current; current = current.directParent()) { if (current.internalKind() == DomType::ScriptLabelledStatement) { const QString label = current.field(Fields::label).value().toString(); if (label.isEmpty()) continue; CompletionItem item; item.label = label.toUtf8(); item.kind = int(CompletionItemKind::Value); // variable? // TODO: more stuff here? result = item; } else if (current.internalKind() == DomType::ScriptExpression) { // quick exit when leaving the JS part return; } } return; } void QQmlLSCompletion::insideContinueStatement(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation continueKeyword = regions[ContinueKeywordRegion]; if (afterLocation(continueKeyword, positionInfo)) { collectLabels(parentForContext, result); return; } } void QQmlLSCompletion::insideBreakStatement(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation breakKeyword = regions[BreakKeywordRegion]; if (afterLocation(breakKeyword, positionInfo)) { collectLabels(parentForContext, result); return; } } void QQmlLSCompletion::insideConditionalExpression(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation questionMark = regions[QuestionMarkTokenRegion]; const QQmlJS::SourceLocation colon = regions[ColonTokenRegion]; if (beforeLocation(positionInfo, questionMark)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (betweenLocations(questionMark, positionInfo, colon)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } if (afterLocation(colon, positionInfo)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideUnaryExpression(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation operatorToken = regions[OperatorTokenRegion]; if (afterLocation(operatorToken, positionInfo)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insidePostExpression(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation operatorToken = regions[OperatorTokenRegion]; if (beforeLocation(positionInfo, operatorToken)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::insideParenthesizedExpression(const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, BackInsertIterator result) const { const auto regions = FileLocations::treeOf(parentForContext)->info().regions; const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); return; } } void QQmlLSCompletion::signalHandlerCompletion(const QQmlJSScope::ConstPtr &scope, QDuplicateTracker *usedNames, BackInsertIterator result) const { const auto keyValues = scope->methods().asKeyValueRange(); for (const auto &[name, method] : keyValues) { if (method.access() != QQmlJSMetaMethod::Public || method.methodType() != QQmlJSMetaMethodType::Signal) { continue; } if (usedNames && usedNames->hasSeen(name)) { continue; } CompletionItem completion; completion.label = QQmlSignalNames::signalNameToHandlerName(name).toUtf8(); completion.kind = int(CompletionItemKind::Method); result = completion; } } /*! \internal Decide which completions can be used at currentItem and compute them. */ QList QQmlLSCompletion::completions(const DomItem ¤tItem, const CompletionContextStrings &contextStrings) const { QList result; collectCompletions(currentItem, contextStrings, std::back_inserter(result)); return result; } void QQmlLSCompletion::collectCompletions(const DomItem ¤tItem, const CompletionContextStrings &contextStrings, BackInsertIterator result) const { /*! Completion is not provided on a script identifier expression because script identifier expressions lack context information. Instead, find the first parent that has enough context information and provide completion for this one. For example, a script identifier expression \c{le} in \badcode for (;le;) { ... } \endcode will get completion for a property called \c{leProperty}, while the same script identifier expression in \badcode for (le;;) { ... } \endcode will, in addition to \c{leProperty}, also get completion for the \c{let} statement snippet. In this example, the parent used for the completion is the for-statement, of type DomType::ScriptForStatement. In addition of the parent for the context, use positionInfo to have exact information on where the cursor is (to compare with the SourceLocations of tokens) and which item is at this position (required to provide completion at the correct position, for example for attached properties). */ const QQmlLSCompletionPosition positionInfo{ currentItem, contextStrings }; for (DomItem currentParent = currentItem; currentParent; currentParent = currentParent.directParent()) { const DomType currentType = currentParent.internalKind(); switch (currentType) { case DomType::Id: // suppress completions for ids return; case DomType::Pragma: insidePragmaCompletion(currentParent, positionInfo, result); return; case DomType::ScriptType: { if (currentParent.directParent().internalKind() == DomType::QmlObject) { insideQmlObjectCompletion(currentParent.directParent(), positionInfo, result); return; } LocalSymbolsTypes options; options.setFlag(LocalSymbolsType::ObjectType); options.setFlag(LocalSymbolsType::ValueType); suggestReachableTypes(currentItem, options, CompletionItemKind::Class, result); return; } case DomType::ScriptFormalParameter: // no autocompletion inside of function parameter definition return; case DomType::Binding: insideBindingCompletion(currentParent, positionInfo, result); return; case DomType::Import: insideImportCompletion(currentParent, positionInfo, result); return; case DomType::ScriptForStatement: insideForStatementCompletion(currentParent, positionInfo, result); return; case DomType::ScriptBlockStatement: suggestJSStatementCompletion(positionInfo.itemAtPosition, result); return; case DomType::QmlFile: insideQmlFileCompletion(currentParent, positionInfo, result); return; case DomType::QmlObject: insideQmlObjectCompletion(currentParent, positionInfo, result); return; case DomType::MethodInfo: // suppress completions return; case DomType::PropertyDefinition: insidePropertyDefinitionCompletion(currentParent, positionInfo, result); return; case DomType::ScriptBinaryExpression: // ignore field member expressions: these need additional context from its parents if (QQmlLSUtils::isFieldMemberExpression(currentParent)) continue; insideBinaryExpressionCompletion(currentParent, positionInfo, result); return; case DomType::ScriptLiteral: insideScriptLiteralCompletion(currentParent, positionInfo, result); return; case DomType::ScriptCallExpression: insideCallExpression(currentParent, positionInfo, result); return; case DomType::ScriptIfStatement: insideIfStatement(currentParent, positionInfo, result); return; case DomType::ScriptReturnStatement: insideReturnStatement(currentParent, positionInfo, result); return; case DomType::ScriptWhileStatement: insideWhileStatement(currentParent, positionInfo, result); return; case DomType::ScriptDoWhileStatement: insideDoWhileStatement(currentParent, positionInfo, result); return; case DomType::ScriptForEachStatement: insideForEachStatement(currentParent, positionInfo, result); return; case DomType::ScriptTryCatchStatement: /*! \internal The Ecmascript standard specifies that there can only be a block statement between \c try and \c catch(...), \c try and \c finally and \c catch(...) and \c finally, so all of these completions are already handled by the DomType::ScriptBlockStatement completion. The only place in the try statement where there is no BlockStatement and therefore needs its own completion is inside the catch parameter, but that is \quotation An optional identifier or pattern to hold the caught exception for the associated catch block. \endquotation citing https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch?retiredLocale=de#exceptionvar. This means that no completion is needed inside a catch-expression, as it should contain an identifier that is not yet used anywhere. Therefore, no completion is required at all when inside a try-statement but outside a block-statement. */ return; case DomType::ScriptSwitchStatement: insideSwitchStatement(currentParent, positionInfo, result); return; case DomType::ScriptCaseClause: insideCaseClause(currentParent, positionInfo, result); return; case DomType::ScriptDefaultClause: if (ctxBeforeStatement(positionInfo, currentParent, QQmlJS::Dom::DefaultKeywordRegion)) continue; insideDefaultClause(currentParent, positionInfo, result); return; case DomType::ScriptCaseBlock: insideCaseBlock(currentParent, positionInfo, result); return; case DomType::ScriptVariableDeclaration: // not needed: thats a list of ScriptVariableDeclarationEntry, and those entries cannot // be suggested because they all start with `{`, `[` or an identifier that should not be // in use yet. return; case DomType::ScriptVariableDeclarationEntry: insideVariableDeclarationEntry(currentParent, positionInfo, result); return; case DomType::ScriptProperty: // fallthrough: a ScriptProperty is a ScriptPattern but inside a JS Object. It gets the // same completions as a ScriptPattern. case DomType::ScriptPattern: insideScriptPattern(currentParent, positionInfo, result); return; case DomType::ScriptThrowStatement: insideThrowStatement(currentParent, positionInfo, result); return; case DomType::ScriptLabelledStatement: insideLabelledStatement(currentParent, positionInfo, result); return; case DomType::ScriptContinueStatement: insideContinueStatement(currentParent, positionInfo, result); return; case DomType::ScriptBreakStatement: insideBreakStatement(currentParent, positionInfo, result); return; case DomType::ScriptConditionalExpression: insideConditionalExpression(currentParent, positionInfo, result); return; case DomType::ScriptUnaryExpression: insideUnaryExpression(currentParent, positionInfo, result); return; case DomType::ScriptPostExpression: insidePostExpression(currentParent, positionInfo, result); return; case DomType::ScriptParenthesizedExpression: insideParenthesizedExpression(currentParent, positionInfo, result); return; // TODO: Implement those statements. // In the meanwhile, suppress completions to avoid weird behaviors. case DomType::ScriptArray: case DomType::ScriptObject: case DomType::ScriptElision: case DomType::ScriptArrayEntry: return; default: continue; } Q_UNREACHABLE(); } // no completion could be found qCDebug(QQmlLSUtilsLog) << "No completion was found for current request."; return; } QQmlLSCompletion::QQmlLSCompletion(const QFactoryLoader &pluginLoader) { const auto keys = pluginLoader.metaDataKeys(); for (qsizetype i = 0; i < keys.size(); ++i) { auto instance = std::unique_ptr( qobject_cast(pluginLoader.instance(i))); if (!instance) continue; if (auto completionInstance = instance->createCompletionPlugin()) m_plugins.push_back(std::move(completionInstance)); } } /*! \internal Helper method to call a method on all loaded plugins. */ void QQmlLSCompletion::collectFromPlugins(qxp::function_ref f, BackInsertIterator result) const { for (const auto &plugin : m_plugins) { Q_ASSERT(plugin); f(plugin.get(), result); } } void QQmlLSCompletion::suggestSnippetsForLeftHandSideOfBinding(const DomItem &itemAtPosition, BackInsertIterator result) const { collectFromPlugins( [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) { p->suggestSnippetsForLeftHandSideOfBinding(itemAtPosition, result); }, result); } void QQmlLSCompletion::suggestSnippetsForRightHandSideOfBinding(const DomItem &itemAtPosition, BackInsertIterator result) const { collectFromPlugins( [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) { p->suggestSnippetsForRightHandSideOfBinding(itemAtPosition, result); }, result); } QT_END_NAMESPACE