aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/qmlcompiler/qqmljsscope.cpp9
-rw-r--r--src/qmlcompiler/qqmljsscope_p.h1
-rw-r--r--src/qmlls/qqmlcompletioncontextstrings_p.h2
-rw-r--r--src/qmlls/qqmllsutils.cpp410
-rw-r--r--src/qmlls/qqmllsutils_p.h11
-rw-r--r--tests/auto/qml/qmlformat/tst_qmlformat.cpp1
-rw-r--r--tests/auto/qmlls/modules/tst_qmlls_modules.cpp10
-rw-r--r--tests/auto/qmlls/utils/data/Yyy.qml39
-rw-r--r--tests/auto/qmlls/utils/data/Zzz.qml2
-rw-r--r--tests/auto/qmlls/utils/data/emptyFile.qml0
-rw-r--r--tests/auto/qmlls/utils/tst_qmlls_utils.cpp236
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 &currentItem,
+ const CompletionContextStrings &ctx)
+{
+ auto fileLocations = FileLocations::treeOf(currentItem)->info().fullRegion;
+ return ctx.offset() <= fileLocations.offset;
+}
+
QList<CompletionItem> QQmlLSUtils::completions(const DomItem &currentItem,
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))