aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSami Shalayel <sami.shalayel@qt.io>2023-09-11 15:50:26 +0200
committerSami Shalayel <sami.shalayel@qt.io>2023-09-26 20:46:38 +0200
commit8237998bf4e22571a2dd6d80ea897171fed762af (patch)
tree61ad1e5e5e803184463aa53b77b71849415f32fe
parente3ffea6d42191e5b2d00aab9cfbd628913af4754 (diff)
qmlls: autocomplete scriptexpressions
Add some support for autocompletion in script expressions. Add missing getter in QQmlJSScope to be able to list js identifiers of a semantic scope in qmlls. Add static helper methods XXXCompletion that collect some information JavaScript identifiers, methods and properties made available from a QQmlJSScope. Another helper method collectFromAllJavaScriptParents can collect the results of a XXXCompletion method through all JavaScript scopes of a DomItem. Avoid code duplication and remove the implementation searching for methods in the DOM: instead, use the already existing implementation in QQmlJSScope::methods. Finally, add the method scriptIdentifierCompletion() that computes all autocompletions for a scriptIdentifier, that is called in QQmlLSUtils::completions and in the tests. Fix some tests to flag property as properties, inside of "Fields", and add extra tests to see if the newly implemented completions work. Cleanup QQmlLSUtils::completions() by extracting its code into static helper methods reachableTypes() and reachableMethods(), replace two enums by one QFlag as both enums were always used together. Extend reachableTypes() to find also non-object types, like the value types int, date, for example. This is later used for property type or function parameter type completion, for example. Make QQmlLSUtils::completions() more readable by returning early and by separating the completion by QML language constructs, instead of grouping multiple unrelated constructs together. This became possible thanks to the new static helpers mentioned above. Suppress completion inside of function parameter definitions and inside of id definitions. Add some tests for property type completion and pragma completion with QEXPECT_FAIL, those features will be implemented in a later commit. Add 'import' completion for empty files + a test that test completions on an empty file. Fix tests for colon-checking: some completions insert '<propertyName>: ' for properties, e.g. for bindings, and some do not, e.g. for property usage in JS expressions. Also fix the test in tst_qmlls_modules to expect methods instead of functions. Add exception in tst_qmlformat for the empty qml file used to test if completions work in a completely empty file. Task-number: QTBUG-116899 Change-Id: I63de62c71d63aa4ab62ca6d83c6be157f4e6f96c Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
-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))