diff options
-rw-r--r-- | src/qmlcompiler/qqmljsscope.cpp | 9 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljsscope_p.h | 1 | ||||
-rw-r--r-- | src/qmlls/qqmlcompletioncontextstrings_p.h | 2 | ||||
-rw-r--r-- | src/qmlls/qqmllsutils.cpp | 410 | ||||
-rw-r--r-- | src/qmlls/qqmllsutils_p.h | 11 | ||||
-rw-r--r-- | tests/auto/qml/qmlformat/tst_qmlformat.cpp | 1 | ||||
-rw-r--r-- | tests/auto/qmlls/modules/tst_qmlls_modules.cpp | 10 | ||||
-rw-r--r-- | tests/auto/qmlls/utils/data/Yyy.qml | 39 | ||||
-rw-r--r-- | tests/auto/qmlls/utils/data/Zzz.qml | 2 | ||||
-rw-r--r-- | tests/auto/qmlls/utils/data/emptyFile.qml | 0 | ||||
-rw-r--r-- | tests/auto/qmlls/utils/tst_qmlls_utils.cpp | 236 |
11 files changed, 537 insertions, 184 deletions
diff --git a/src/qmlcompiler/qqmljsscope.cpp b/src/qmlcompiler/qqmljsscope.cpp index 4664e62c54..b247ee0570 100644 --- a/src/qmlcompiler/qqmljsscope.cpp +++ b/src/qmlcompiler/qqmljsscope.cpp @@ -63,6 +63,15 @@ QQmlJSScope::Ptr QQmlJSScope::clone(const ConstPtr &origin) return cloned; } +/*! +\internal +Return all the JavaScript identifiers defined in the current scope. +*/ +QHash<QString, QQmlJSScope::JavaScriptIdentifier> QQmlJSScope::ownJSIdentifiers() const +{ + return m_jsIdentifiers; +} + void QQmlJSScope::insertJSIdentifier(const QString &name, const JavaScriptIdentifier &identifier) { Q_ASSERT(m_scopeType != QQmlSA::ScopeType::QMLScope); diff --git a/src/qmlcompiler/qqmljsscope_p.h b/src/qmlcompiler/qqmljsscope_p.h index e21a8781d2..baebc2f973 100644 --- a/src/qmlcompiler/qqmljsscope_p.h +++ b/src/qmlcompiler/qqmljsscope_p.h @@ -208,6 +208,7 @@ public: static void reparent(const QQmlJSScope::Ptr &parentScope, const QQmlJSScope::Ptr &childScope); void insertJSIdentifier(const QString &name, const JavaScriptIdentifier &identifier); + QHash<QString, JavaScriptIdentifier> ownJSIdentifiers() const; void insertPropertyIdentifier(const QQmlJSMetaProperty &prop); ScopeType scopeType() const { return m_scopeType; } diff --git a/src/qmlls/qqmlcompletioncontextstrings_p.h b/src/qmlls/qqmlcompletioncontextstrings_p.h index 1bd4dc8f22..78cf2b1553 100644 --- a/src/qmlls/qqmlcompletioncontextstrings_p.h +++ b/src/qmlls/qqmlcompletioncontextstrings_p.h @@ -46,6 +46,8 @@ public: // if we are at line start bool atLineStart() const { return m_atLineStart; } + qsizetype offset() const { return m_pos; } + private: QString m_code; // the current code qsizetype m_pos = {}; // current position of the cursor diff --git a/src/qmlls/qqmllsutils.cpp b/src/qmlls/qqmllsutils.cpp index ef52b77900..b9e235de31 100644 --- a/src/qmlls/qqmllsutils.cpp +++ b/src/qmlls/qqmllsutils.cpp @@ -1660,100 +1660,51 @@ QList<CompletionItem> QQmlLSUtils::idsCompletions(const DomItem& component) return res; } +static void reachableTypes(QSet<QString> &symbols, const DomItem &el, LocalSymbolsTypes options) +{ + switch (el.internalKind()) { + case DomType::ImportScope: { + + const QSet<QString> localSymbols = el.localSymbolNames(options); + qCDebug(QQmlLSCompletionLog) << "adding local symbols of:" << el.internalKindStr() + << el.canonicalPath() << localSymbols; + symbols += localSymbols; + break; + } + default: { + qCDebug(QQmlLSCompletionLog) << "skipping local symbols for non type" + << el.internalKindStr() << el.canonicalPath(); + break; + } + } +} + QList<CompletionItem> QQmlLSUtils::reachableSymbols(const DomItem &context, const CompletionContextStrings &ctx, - TypeCompletionsType typeCompletionType, - FunctionCompletion completeMethodCalls) + TypeCompletionOptions typeCompletionType) { // returns completions for the reachable types or attributes from context QList<CompletionItem> res; QMap<CompletionItemKind, QSet<QString>> symbols; QSet<quintptr> visited; QList<Path> visitedRefs; - auto addLocalSymbols = [&res, typeCompletionType, completeMethodCalls, &symbols](const DomItem &el) { - switch (typeCompletionType) { - case TypeCompletionsType::None: - return false; - case TypeCompletionsType::Types: - switch (el.internalKind()) { - case DomType::ImportScope: { - const QSet<QString> localSymbols = el.localSymbolNames( - LocalSymbolsType::QmlTypes | LocalSymbolsType::Namespaces); - qCDebug(QQmlLSCompletionLog) << "adding local symbols of:" << el.internalKindStr() - << el.canonicalPath() << localSymbols; - symbols[CompletionItemKind::Class] += localSymbols; - break; - } - default: { - qCDebug(QQmlLSCompletionLog) << "skipping local symbols for non type" - << el.internalKindStr() << el.canonicalPath(); - break; - } - } - break; - case TypeCompletionsType::TypesAndAttributes: - auto localSymbols = el.localSymbolNames(LocalSymbolsType::All); - if (const QmlObject *elPtr = el.as<QmlObject>()) { - auto methods = elPtr->methods(); - auto it = methods.cbegin(); - while (it != methods.cend()) { - localSymbols.remove(it.key()); - if (completeMethodCalls == FunctionCompletion::Declaration) { - QStringList parameters; - for (const MethodParameter &pInfo : std::as_const(it->parameters)) { - QStringList param; - if (!pInfo.typeName.isEmpty()) - param << pInfo.typeName; - if (!pInfo.name.isEmpty()) - param << pInfo.name; - if (pInfo.defaultValue) { - param << u"= " + pInfo.defaultValue->code(); - } - parameters.append(param.join(u' ')); - } - - QString commentsStr; - - if (!it->comments.regionComments.isEmpty()) { - for (const Comment &c : - it->comments.regionComments[QString()].preComments) { - commentsStr += c.rawComment().toString().trimmed() + u'\n'; - } - } - - CompletionItem comp; - comp.documentation = - u"%1%2(%3)"_s.arg(commentsStr, it.key(), parameters.join(u", ")) - .toUtf8(); - comp.label = (it.key() + u"()").toUtf8(); - comp.kind = int(CompletionItemKind::Function); - - if (it->typeName.isEmpty()) - comp.detail = "returns void"; - else - comp.detail = (u"returns "_s + it->typeName).toUtf8(); - - // Only append full bracket if there are no parameters - if (it->parameters.isEmpty()) - comp.insertText = comp.label; - else - // add snippet support? - comp.insertText = (it.key() + u"(").toUtf8(); - - res.append(comp); - } - ++it; - } - } - qCDebug(QQmlLSCompletionLog) << "adding local symbols of:" << el.internalKindStr() - << el.canonicalPath() << localSymbols; - symbols[CompletionItemKind::Field] += localSymbols; - break; + auto addLocalSymbols = [&typeCompletionType, &symbols](const DomItem &el) { + LocalSymbolsTypes options; + if (typeCompletionType.testFlag(TypeCompletionOption::Types)) { + options.setFlag(LocalSymbolsType::Namespaces); + options.setFlag(LocalSymbolsType::Types); + } + if (typeCompletionType.testFlag(TypeCompletionOption::QmlTypes)) { + options.setFlag(LocalSymbolsType::QmlTypes); + options.setFlag(LocalSymbolsType::Namespaces); + } + if (options != LocalSymbolsType::None) { + reachableTypes(symbols[CompletionItemKind::Class], el, options); } return true; }; if (ctx.base().isEmpty()) { - if (typeCompletionType != TypeCompletionsType::None) { + if (typeCompletionType != TypeCompletionOption::None) { qCDebug(QQmlLSCompletionLog) << "adding symbols reachable from:" << context.internalKindStr() << context.canonicalPath(); @@ -1789,69 +1740,278 @@ QList<CompletionItem> QQmlLSUtils::reachableSymbols(const DomItem &context, return res; } +static QList<CompletionItem> jsIdentifierCompletion(const QQmlJSScope *scope, + QDuplicateTracker<QString> *usedNames) +{ + QList<CompletionItem> result; + 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(); + result.append(completion); + } + return result; +} + +static QList<CompletionItem> methodCompletion(const QQmlJSScope *scope, + QDuplicateTracker<QString> *usedNames) +{ + QList<CompletionItem> result; + // 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); + result.append(completion); + // TODO: QQmlLSUtils::reachableSymbols seems to be able to do documentation and detail + // and co, it should also be done here if possible. + } + return result; +} + +static QList<CompletionItem> propertyCompletion(const QQmlJSScope *scope, + QDuplicateTracker<QString> *usedNames) +{ + QList<CompletionItem> result; + 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(); + result.append(completion); + } + return 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<auto F, typename... T> +decltype(auto) collectFromAllJavaScriptParents(const QQmlJSScope *scope, T... args) +{ + decltype(F(scope, args...)) result; + for (const QQmlJSScope *current = scope; current; current = current->parentScope().get()) { + result << F(current, args...); + if (current->scopeType() == QQmlSA::ScopeType::QMLScope) + break; + } + return result; +} + +QList<CompletionItem> QQmlLSUtils::scriptIdentifierCompletion(const DomItem &context, + const CompletionContextStrings &ctx) +{ + QList<CompletionItem> result; + QDuplicateTracker<QString> usedNames; + const QQmlJSScope *nearestScope; + const bool hasQualifier = !ctx.base().isEmpty(); + + if (!hasQualifier) { + result << idsCompletions(context.component()); + + auto scope = context.nearestSemanticScope(); + if (!scope) + return result; + nearestScope = scope.get(); + } else { + auto expressionType = QQmlLSUtils::resolveExpressionType(context, ResolveOwnerType); + if (!expressionType) + return result; + nearestScope = expressionType->semanticScope.get(); + } + + if (!nearestScope) + return result; + + result << methodCompletion(nearestScope, &usedNames) + << propertyCompletion(nearestScope, &usedNames); + + if (!hasQualifier) { + // collect all of the stuff from parents + result << collectFromAllJavaScriptParents<jsIdentifierCompletion>(nearestScope, &usedNames) + << collectFromAllJavaScriptParents<methodCompletion>(nearestScope, &usedNames) + << collectFromAllJavaScriptParents<propertyCompletion>(nearestScope, &usedNames); + } + + return result; +} + +static const QQmlJSScope *resolve(const QQmlJSScope *current, 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; +} + +static bool cursorInFrontOfItem(const DomItem ¤tItem, + const CompletionContextStrings &ctx) +{ + auto fileLocations = FileLocations::treeOf(currentItem)->info().fullRegion; + return ctx.offset() <= fileLocations.offset; +} + QList<CompletionItem> QQmlLSUtils::completions(const DomItem ¤tItem, const CompletionContextStrings &ctx) { - QList<CompletionItem> res; - DomItem containingObject = currentItem.qmlObject(); - DomItem containingFile = currentItem.containingFile(); - TypeCompletionsType typeCompletionType = TypeCompletionsType::None; - FunctionCompletion methodCompletion = FunctionCompletion::Declaration; + if (currentItem.internalKind() == DomType::Id) { + // suppress completions for ids + return {}; + } - if (!containingObject) { - methodCompletion = FunctionCompletion::None; - // global completions - if (ctx.atLineStart()) { - if (ctx.base().isEmpty()) { - { - CompletionItem comp; - comp.label = "pragma"; - comp.kind = int(CompletionItemKind::Keyword); - res.append(comp); + if (currentItem.internalKind() == DomType::Pragma) { + return {}; + } + + const DomItem containingType = currentItem.filterUp( + [](DomType type, const QQmlJS::Dom::DomItem &) { return type == DomType::ScriptType; }, + FilterUpOptions::ReturnInner); + if (containingType) { + TypeCompletionOptions typeCompletionType; + typeCompletionType.setFlag(TypeCompletionOption::Types); + typeCompletionType.setFlag(TypeCompletionOption::QmlTypes); + return reachableSymbols(currentItem, ctx, typeCompletionType); + } + + const DomItem containingParameter = currentItem.filterUp( + [](DomType type, const QQmlJS::Dom::DomItem &) { + return type == DomType::ScriptFormalParameter; + }, + FilterUpOptions::ReturnInner); + if (containingParameter) { + // no autocompletion inside of function parameter definition + return {}; + } + + const DomItem containingScriptExpression = currentItem.containingScriptExpression(); + if (containingScriptExpression) { + return scriptIdentifierCompletion(currentItem, ctx); + } + const DomItem containingObject = currentItem.qmlObject(); + const DomItem containingFile = currentItem.containingFile(); + if (currentItem.internalKind() == DomType::Binding) { + QList<CompletionItem> res; + // do scriptidentifiercompletion after the ':' of a binding + auto location = FileLocations::treeOf(currentItem)->info(); + auto region = location.regions.constFind(u"colon"_s); + + if (region != location.regions.constEnd()) { + if (region.value().isValid() && region.value().offset < ctx.offset()) { + QList<CompletionItem> res; + res << scriptIdentifierCompletion(currentItem, ctx); + if (auto type = 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) { + res << reachableSymbols(currentItem, ctx, TypeCompletionOption::QmlTypes); + } } + return res; } - typeCompletionType = TypeCompletionsType::Types; } - // Import completion + res << bindingsCompletions(containingObject); + // add Qml Types for default binding + res += reachableSymbols(containingFile, ctx, TypeCompletionOption::QmlTypes); + return res; + } + + if (currentItem.internalKind() == DomType::Import) { + QList<CompletionItem> res; res += importCompletions(containingFile, ctx); - } else { - methodCompletion = FunctionCompletion::Declaration; - bool addIds = false; - if (ctx.atLineStart() && currentItem.internalKind() != DomType::ScriptExpression - && currentItem.internalKind() != DomType::List) { - // add bindings - methodCompletion = FunctionCompletion::None; + // when in front of the import statement: propose types for root Qml Object completion + if (cursorInFrontOfItem(currentItem, ctx)) + res += reachableSymbols(containingFile, ctx, TypeCompletionOption::QmlTypes); + + return res; + } + + if (!containingObject) { + QList<CompletionItem> res; + // completions for code outside the root Qml Object + // global completions + if (ctx.atLineStart()) { if (ctx.base().isEmpty()) { - for (const QStringView &s : std::array<QStringView, 5>( - { u"property", u"readonly", u"default", u"signal", u"function" })) { + for (const QStringView &s : std::array<QStringView, 2>({ u"pragma", u"import" })) { CompletionItem comp; comp.label = s.toUtf8(); comp.kind = int(CompletionItemKind::Keyword); res.append(comp); } - res += bindingsCompletions(containingObject); - typeCompletionType = TypeCompletionsType::Types; - } else { - // handle value types later with type expansion - typeCompletionType = TypeCompletionsType::TypesAndAttributes; } - } else { - addIds = true; - typeCompletionType = TypeCompletionsType::TypesAndAttributes; - } - if (addIds) { - res += idsCompletions(containingObject.component()); } + // Types for root Qml Object completion + res += reachableSymbols(containingFile, ctx, TypeCompletionOption::QmlTypes); + return res; } - DomItem context = containingObject; - if (!context) - context = containingFile; - // adds types and attributes - res += reachableSymbols(context, ctx, typeCompletionType, methodCompletion); + if (ctx.atLineStart() && currentItem.internalKind() != DomType::List) { + // inside some Qml Object + QList<CompletionItem> res; + if (ctx.base().isEmpty()) { + // TODO: complete also the brackets after function? + for (const QStringView &s : std::array<QStringView, 5>( + { u"property", u"readonly", u"default", u"signal", u"function" })) { + CompletionItem comp; + comp.label = s.toUtf8(); + comp.kind = int(CompletionItemKind::Keyword); + res.append(comp); + } + // add bindings + res += bindingsCompletions(containingObject); + // add Qml Types for default binding + res += reachableSymbols(containingFile, ctx, TypeCompletionOption::QmlTypes); + } + return res; + } - return res; + // no completion could be found + qCDebug(QQmlLSUtilsLog) << "No completion was found for current request."; + return {}; } QT_END_NAMESPACE diff --git a/src/qmlls/qqmllsutils_p.h b/src/qmlls/qqmllsutils_p.h index b943d74b77..3ffba2ff15 100644 --- a/src/qmlls/qqmllsutils_p.h +++ b/src/qmlls/qqmllsutils_p.h @@ -116,9 +116,8 @@ enum QQmlLSUtilsResolveOptions { ResolveActualTypeForFieldMemberExpression, }; -enum class TypeCompletionsType { None, Types, TypesAndAttributes }; - -enum class FunctionCompletion { None, Declaration }; +enum class TypeCompletionOption { None, Types, QmlTypes, TypesAndAttributes }; +Q_DECLARE_FLAGS(TypeCompletionOptions, TypeCompletionOption); enum class ImportCompletionType { None, Module, Version }; @@ -163,8 +162,10 @@ public: static QList<CompletionItem> reachableSymbols(const DomItem &context, const CompletionContextStrings &ctx, - TypeCompletionsType typeCompletionType, - FunctionCompletion completeMethodCalls); + TypeCompletionOptions typeCompletionType); + + static QList<CompletionItem> scriptIdentifierCompletion(const DomItem &context, + const CompletionContextStrings &ctx); static QList<CompletionItem> completions(const DomItem& currentItem, const CompletionContextStrings &ctx); }; diff --git a/tests/auto/qml/qmlformat/tst_qmlformat.cpp b/tests/auto/qml/qmlformat/tst_qmlformat.cpp index a763e2b5cc..c5c51f178c 100644 --- a/tests/auto/qml/qmlformat/tst_qmlformat.cpp +++ b/tests/auto/qml/qmlformat/tst_qmlformat.cpp @@ -129,6 +129,7 @@ void TestQmlformat::initTestCase() m_invalidFiles << "tests/auto/qml/qqmllanguage/data/nullishCoalescing_RHS_Or.qml"; m_invalidFiles << "tests/auto/qml/qqmllanguage/data/typeAnnotations.2.qml"; m_invalidFiles << "tests/auto/qml/qqmlparser/data/disallowedtypeannotations/qmlnestedfunction.qml"; + m_invalidFiles << "tests/auto/qmlls/utils/data/emptyFile.qml"; // Files that get changed: // rewrite of import "bla/bla/.." to import "bla" diff --git a/tests/auto/qmlls/modules/tst_qmlls_modules.cpp b/tests/auto/qmlls/modules/tst_qmlls_modules.cpp index 51fa1e6883..50a3078564 100644 --- a/tests/auto/qmlls/modules/tst_qmlls_modules.cpp +++ b/tests/auto/qmlls/modules/tst_qmlls_modules.cpp @@ -259,10 +259,10 @@ void tst_qmlls_modules::function_documentations_data() QTest::newRow("longfunction") << filePath << 5 << 14 << ExpectedDocumentations{ - std::make_tuple(u"lala()"_s, u"returns void"_s, u"lala()"_s), - std::make_tuple(u"longfunction()"_s, u"returns string"_s, + std::make_tuple(u"lala"_s, u"returns void"_s, u"lala()"_s), + std::make_tuple(u"longfunction"_s, u"returns string"_s, uR"(longfunction(a, b, c = "c", d = "d"))"_s), - std::make_tuple(u"documentedFunction()"_s, u"returns string"_s, + std::make_tuple(u"documentedFunction"_s, u"returns string"_s, uR"(// documentedFunction: is documented // returns 'Good' documentedFunction(arg1, arg2 = "Qt"))"_s), @@ -301,7 +301,7 @@ void tst_qmlls_modules::function_documentations() bool hasFoundExpected = false; const auto expectedLabel = std::get<0>(exp); for (const CompletionItem &c : *cItems) { - if (c.kind->toInt() != int(CompletionItemKind::Function)) { + if (c.kind->toInt() != int(CompletionItemKind::Method)) { // Only check functions. continue; } @@ -348,7 +348,7 @@ void tst_qmlls_modules::function_documentations() QVERIFY2(false, "error computing the completion"); clean(); }); - QTRY_VERIFY_WITH_TIMEOUT(*didFinish, 30000); + QTRY_VERIFY_WITH_TIMEOUT(*didFinish, 3000); } void tst_qmlls_modules::buildDir() diff --git a/tests/auto/qmlls/utils/data/Yyy.qml b/tests/auto/qmlls/utils/data/Yyy.qml index 23bbd057f7..eb4f290709 100644 --- a/tests/auto/qmlls/utils/data/Yyy.qml +++ b/tests/auto/qmlls/utils/data/Yyy.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import QtQuick as QQ - +pragma Singleton Zzz { id: root width: height @@ -26,4 +26,41 @@ Zzz { QQ.Rectangle { color:"red" } + + Item { + id: someItem + property int helloProperty + } + + function parameterCompletion(helloWorld, helloMe: int) { + let helloVar = 42; + let result = someItem.helloProperty + helloWorld; + return result; + } + + component Base: QtObject { + property int propertyInBase + function functionInBase(jsParameterInBase) { + let jsIdentifierInBase; + return jsIdentifierInBase; + } + } + + Base { + property int propertyInDerived + function functionInDerived(jsParameterInDerived) { + let jsIdentifierInDerived; + return jsIdentifierInDerived; + } + + property Base child: Base { + property int propertyInChild + function functionInChild(jsParameterInChild) { + let jsIdentifierInChild; + return someItem.helloProperty; + } + } + } + function test1() { for (myvar = 42; i<0; ++i){} console.log(myvar);} + function test2() { for (var myvar = 42; i<0; ++i){} console.log(myvar);} } diff --git a/tests/auto/qmlls/utils/data/Zzz.qml b/tests/auto/qmlls/utils/data/Zzz.qml index 165ea46394..fa0edf69dc 100644 --- a/tests/auto/qmlls/utils/data/Zzz.qml +++ b/tests/auto/qmlls/utils/data/Zzz.qml @@ -7,4 +7,6 @@ Item { Rectangle { width: zzz.height } + + property int propertyOfZZZ } diff --git a/tests/auto/qmlls/utils/data/emptyFile.qml b/tests/auto/qmlls/utils/data/emptyFile.qml new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/auto/qmlls/utils/data/emptyFile.qml diff --git a/tests/auto/qmlls/utils/tst_qmlls_utils.cpp b/tests/auto/qmlls/utils/tst_qmlls_utils.cpp index cff7ae9fac..f55eb0d3a3 100644 --- a/tests/auto/qmlls/utils/tst_qmlls_utils.cpp +++ b/tests/auto/qmlls/utils/tst_qmlls_utils.cpp @@ -1543,6 +1543,8 @@ void tst_qmlls_utils::isValidEcmaScriptIdentifier() using namespace QLspSpecification; +enum InsertOption { None, InsertColon }; + void tst_qmlls_utils::completions_data() { QTest::addColumn<QString>("filePath"); @@ -1550,8 +1552,10 @@ void tst_qmlls_utils::completions_data() QTest::addColumn<int>("character"); QTest::addColumn<ExpectedCompletions>("expected"); QTest::addColumn<QStringList>("notExpected"); + QTest::addColumn<InsertOption>("insertOptions"); QString file = testFile(u"Yyy.qml"_s); + QString emptyFile = testFile(u"emptyFile.qml"_s); QTest::newRow("objEmptyLine") << file << 9 << 1 << ExpectedCompletions({ @@ -1560,52 +1564,51 @@ void tst_qmlls_utils::completions_data() { u"width"_s, CompletionItemKind::Property }, { u"function"_s, CompletionItemKind::Keyword }, }) - << QStringList({ u"QtQuick"_s, u"vector4d"_s }); + << QStringList({ u"QtQuick"_s, u"vector4d"_s }) << InsertColon; - QTest::newRow("inBindingLabel") << file << 6 << 10 - << ExpectedCompletions({ - { u"Rectangle"_s, CompletionItemKind::Class }, - { u"property"_s, CompletionItemKind::Keyword }, - { u"width"_s, CompletionItemKind::Property }, - }) - << QStringList({ u"QtQuick"_s, u"vector4d"_s }); + QTest::newRow("inBindingLabel") + << file << 6 << 10 + << ExpectedCompletions({ + { u"Rectangle"_s, CompletionItemKind::Class }, + { u"width"_s, CompletionItemKind::Property }, + }) + << QStringList({ u"QtQuick"_s, u"vector4d"_s, u"property"_s }) << InsertColon; QTest::newRow("afterBinding") << file << 6 << 11 << ExpectedCompletions({ - { u"Rectangle"_s, CompletionItemKind::Field }, - { u"width"_s, CompletionItemKind::Field }, - { u"vector4d"_s, CompletionItemKind::Field }, + { u"height"_s, CompletionItemKind::Property }, + { u"width"_s, CompletionItemKind::Property }, + { u"Rectangle"_s, CompletionItemKind::Class }, }) - << QStringList({ u"QtQuick"_s, u"property"_s }); + << QStringList({ u"QtQuick"_s, u"property"_s, u"vector4d"_s }) + << None; - // suppress? - QTest::newRow("afterId") << file << 5 << 8 - << ExpectedCompletions({ - { u"import"_s, CompletionItemKind::Keyword }, - }) + QTest::newRow("afterId") << file << 5 << 8 << ExpectedCompletions({}) << QStringList({ u"QtQuick"_s, u"property"_s, u"Rectangle"_s, - u"width"_s, u"vector4d"_s }); + u"width"_s, u"vector4d"_s, u"import"_s }) + << None; - QTest::newRow("fileStart") << file << 1 << 1 + QTest::newRow("emptyFile") << emptyFile << 1 << 1 << ExpectedCompletions({ - { u"Rectangle"_s, CompletionItemKind::Class }, { u"import"_s, CompletionItemKind::Keyword }, + { u"pragma"_s, CompletionItemKind::Keyword }, }) - << QStringList({ u"QtQuick"_s, u"vector4d"_s, u"width"_s }); + << QStringList({ u"QtQuick"_s, u"vector4d"_s, u"width"_s }) << None; QTest::newRow("importImport") << file << 1 << 4 << ExpectedCompletions({ - { u"Rectangle"_s, CompletionItemKind::Class }, { u"import"_s, CompletionItemKind::Keyword }, }) - << QStringList({ u"QtQuick"_s, u"vector4d"_s, u"width"_s }); + << QStringList({ u"QtQuick"_s, u"vector4d"_s, u"width"_s, + u"Rectangle"_s }) + << None; QTest::newRow("importModuleStart") << file << 1 << 8 << ExpectedCompletions({ { u"QtQuick"_s, CompletionItemKind::Module }, }) - << QStringList({ u"vector4d"_s, u"width"_s, u"Rectangle"_s, u"import"_s }); + << QStringList({ u"vector4d"_s, u"width"_s, u"Rectangle"_s, u"import"_s }) << None; QTest::newRow("importVersionStart") << file << 1 << 16 @@ -1613,7 +1616,7 @@ void tst_qmlls_utils::completions_data() { u"2"_s, CompletionItemKind::Constant }, { u"as"_s, CompletionItemKind::Keyword }, }) - << QStringList({ u"Rectangle"_s, u"import"_s, u"vector4d"_s, u"width"_s }); + << QStringList({ u"Rectangle"_s, u"import"_s, u"vector4d"_s, u"width"_s }) << None; // QTest::newRow("importVersionMinor") // << uri << 1 << 18 @@ -1622,39 +1625,159 @@ void tst_qmlls_utils::completions_data() // }) // << QStringList({ u"as"_s, u"Rectangle"_s, u"import"_s, u"vector4d"_s, u"width"_s }); - QTest::newRow("inScript") << file << 7 << 15 - << ExpectedCompletions({ - { u"Rectangle"_s, CompletionItemKind::Field }, - { u"vector4d"_s, CompletionItemKind::Field }, - { u"lala()"_s, CompletionItemKind::Function }, - { u"longfunction()"_s, CompletionItemKind::Function }, - { u"documentedFunction()"_s, - CompletionItemKind::Function }, - { u"lala()"_s, CompletionItemKind{ 0 } }, - { u"width"_s, CompletionItemKind::Field }, - }) - << QStringList({ u"import"_s }); - QTest::newRow("expandBase1") << file << 10 << 24 << ExpectedCompletions({ - { u"width"_s, CompletionItemKind::Field }, - { u"foo"_s, CompletionItemKind::Field }, + { u"width"_s, CompletionItemKind::Property }, + { u"foo"_s, CompletionItemKind::Property }, }) - << QStringList({ u"import"_s, u"Rectangle"_s }); + << QStringList({ u"import"_s, u"Rectangle"_s }) << None; QTest::newRow("expandBase2") << file << 11 << 30 << ExpectedCompletions({ - { u"width"_s, CompletionItemKind::Field }, - { u"color"_s, CompletionItemKind::Field }, + { u"width"_s, CompletionItemKind::Property }, + { u"color"_s, CompletionItemKind::Property }, }) - << QStringList({ u"foo"_s, u"import"_s, u"Rectangle"_s }); + << QStringList({ u"foo"_s, u"import"_s, u"Rectangle"_s }) << None; QTest::newRow("asCompletions") << file << 26 << 9 << ExpectedCompletions({ { u"Rectangle"_s, CompletionItemKind::Field }, }) - << QStringList({ u"foo"_s, u"import"_s, u"lala()"_s, u"width"_s }); + << QStringList({ u"foo"_s, u"import"_s, u"lala()"_s, u"width"_s }) << None; + + // TODO: disable completion inside of function arguments + // or only allow it for type completion + QTest::newRow("parameterCompletion") + << file << 36 << 24 + << ExpectedCompletions({ + { u"helloWorld"_s, CompletionItemKind::Variable }, + { u"helloMe"_s, CompletionItemKind::Variable }, + }) + << QStringList() << None; + + QTest::newRow("inParameterCompletion") + << file << 35 << 39 << ExpectedCompletions({}) + << QStringList{ + u"helloWorld"_s, + u"helloMe"_s, + } << None; + + QTest::newRow("propertyTypeCompletion") << file << 16 << 14 << ExpectedCompletions({ + {u"Zzz"_s, CompletionItemKind::Class }, + {u"Item"_s, CompletionItemKind::Class }, + {u"int"_s, CompletionItemKind::Class }, + {u"date"_s, CompletionItemKind::Class }, + }) + << QStringList{ + u"helloWorld"_s, + u"helloMe"_s, + } << None; + + QTest::newRow("parameterTypeCompletion") << file << 35 << 55 << ExpectedCompletions({ + {u"Zzz"_s, CompletionItemKind::Class }, + {u"Item"_s, CompletionItemKind::Class }, + {u"int"_s, CompletionItemKind::Class }, + {u"date"_s, CompletionItemKind::Class }, + }) + << QStringList{ + u"helloWorld"_s, + u"helloMe"_s, + } << None; + + QTest::newRow("qualifiedIdentifierCompletion") + << file << 37 << 36 + << ExpectedCompletions({ + { u"helloProperty"_s, CompletionItemKind::Property }, + { u"childAt"_s, CompletionItemKind::Method }, + }) + << QStringList{ u"helloVar"_s, u"someItem"_s, u"color"_s, u"helloWorld"_s, + u"propertyOfZZZ"_s } + << None; + + QTest::newRow("scriptExpressionCompletion") + << file << 60 << 16 + << ExpectedCompletions({ + // parameters + { u"jsParameterInChild"_s, CompletionItemKind::Variable }, + // own properties + { u"jsIdentifierInChild"_s, CompletionItemKind::Variable }, + { u"functionInChild"_s, CompletionItemKind::Method }, + { u"propertyInChild"_s, CompletionItemKind::Property }, + // inherited properties from QML + { u"functionInBase"_s, CompletionItemKind::Method }, + { u"propertyInBase"_s, CompletionItemKind::Property }, + // inherited properties (transitive) from C++ + { u"objectName"_s, CompletionItemKind::Property }, + { u"someItem"_s, CompletionItemKind::Value }, + }) + << QStringList{ + u"helloVar"_s, + u"color"_s, + u"helloWorld"_s, + u"propertyOfZZZ"_s, + u"propertyInDerived"_s, + u"functionInDerived"_s, + u"jsIdentifierInDerived"_s, + u"jsIdentifierInBase"_s, + u"lala"_s, + u"foo"_s, + u"jsParameterInBase"_s, + u"jsParameterInDerived"_s, + } << None; + + QTest::newRow("qualifiedScriptExpressionCompletion") + << file << 60 << 34 + << ExpectedCompletions({ + // own properties + { u"helloProperty"_s, CompletionItemKind::Property }, + // inherited properties (transitive) from C++ + { u"width"_s, CompletionItemKind::Property }, + }) + << QStringList{ + u"helloVar"_s, + u"color"_s, + u"helloWorld"_s, + u"propertyOfZZZ"_s, + u"propertyInDerived"_s, + u"functionInDerived"_s, + u"jsIdentifierInDerived"_s, + u"jsIdentifierInBase"_s, + u"jsIdentifierInChild"_s, + u"lala"_s, + u"foo"_s, + u"jsParameterInBase"_s, + u"jsParameterInDerived"_s, + u"jsParameterInChild"_s, + u"functionInChild"_s, + } << None; + + QTest::newRow("pragma") + << file << 3 << 8 + << ExpectedCompletions({ + { u"NativeMethodBehavior"_s, CompletionItemKind::Value }, + { u"ComponentBehavior"_s, CompletionItemKind::Value }, + { u"ListPropertyAssignBehavior"_s, CompletionItemKind::Value }, + { u"Singleton"_s, CompletionItemKind::Value }, + // note: only complete the Addressible/Inaddressible part of ValueTypeBehavior! + { u"ValueTypeBehavior"_s, CompletionItemKind::Value }, + }) + << QStringList{ + u"int"_s, + u"Rectangle"_s, + u"FunctionSignatureBehavior"_s, + u"Strict"_s, + } << None; + + QTest::newRow("var-variable") << file << 64 << 67 + << ExpectedCompletions({ + { u"myvar"_s, CompletionItemKind::Value }, + }) + << QStringList{} << None; + QTest::newRow("let-variable") + << file << 3 << 8 + << ExpectedCompletions({}) + << QStringList{ u"myvar"_s, } << None; } void tst_qmlls_utils::completions() @@ -1664,6 +1787,7 @@ void tst_qmlls_utils::completions() QFETCH(int, character); QFETCH(ExpectedCompletions, expected); QFETCH(QStringList, notExpected); + QFETCH(InsertOption, insertOptions); QQmlJS::Dom::DomCreationOptions options; options.setFlag(QQmlJS::Dom::DomCreationOption::WithSemanticAnalysis); @@ -1706,14 +1830,30 @@ void tst_qmlls_utils::completions() QVERIFY2(!fieldsTracker.hasSeen(c.label), "Duplicate field: " + c.label); } else if (c.kind->toInt() == int(CompletionItemKind::Property)) { QVERIFY2(!propertiesTracker.hasSeen(c.label), "Duplicate property: " + c.label); - QVERIFY2(c.insertText == c.label + u": "_s, - "a property should end with a colon with a space for " - "'insertText', for better coding experience"); + if (insertOptions & InsertColon) { + QVERIFY2(c.insertText == c.label + u": "_s, + "a property should end with a colon with a space for " + "'insertText', for better coding experience"); + } else { + QCOMPARE(c.insertText, std::nullopt); + } } labels << c.label; } for (const ExpectedCompletion &exp : expected) { + QEXPECT_FAIL( + "asCompletions", + "Cannot complete after 'QQ.': either there is already a type behind and then " + "there is nothing to complete, or there is nothing behind 'QQ.' and the parser " + "fails because of the unexpected '.'", + Abort); + QEXPECT_FAIL("pragma", "Pragma completion not supported yet", Abort); + QEXPECT_FAIL("propertyTypeCompletion", "No completion for property types supported yet", + Abort); + QEXPECT_FAIL("var-variable", + "Completion for var-variables currently acts the same as for let-variables.", + Abort); QVERIFY2(labels.contains(exp.first), u"no %1 in %2"_s .arg(exp.first, QStringList(labels.begin(), labels.end()).join(u", "_s)) |