Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmllscompletion.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
8
9Q_LOGGING_CATEGORY(QQmlLSCompletionLog, "qt.languageserver.completions")
10
11using namespace QLspSpecification;
12using namespace QQmlJS::Dom;
13using namespace Qt::StringLiterals;
14
15/*!
16\class QQmlLSCompletion
17\internal
18\brief QQmlLSCompletion provides completions for all kinds of QML and JS constructs.
19
20Use the \l{completions} method to obtain completions at a certain DomItem.
21
22All the other methods in this class are helper methods: some compute completions for specific QML
23and JS constructs and some are shared between multiple QML or JS constructs to avoid code
24duplication. Most of the helper methods add their completion items via a BackInsertIterator.
25
26Some helper methods are called "suggest*" and will try to suggest code that does not exist yet. For
27example, any JS statement can be expected inside a Blockstatement so suggestJSStatementCompletion()
28is used to suggest JS statements inside of BlockStatements. Another example might be
29suggestReachableTypes() that will suggest Types for type annotations, attached types or Qml Object
30hierarchies, or suggestCaseAndDefaultStatementCompletion() that will only suggest "case" and
31"default" clauses for switch statements.
32
33Some helper methods are called "inside*" and will try to suggest code inside an existing structure.
34For example, insideForStatementCompletion() will try to suggest completion for the different code
35pieces initializer, condition, increment and statement that exist inside of:
36\badcode
37for(initializer; condition; increment)
38 statement
39\endcode
40*/
41
42CompletionItem QQmlLSCompletion::makeSnippet(QUtf8StringView qualifier, QUtf8StringView label,
43 QUtf8StringView insertText)
44{
45 CompletionItem res;
46 if (!qualifier.isEmpty()) {
47 res.label = qualifier.data();
48 res.label += '.';
49 }
50 res.label += label.data();
51 res.insertTextFormat = InsertTextFormat::Snippet;
52 if (!qualifier.isEmpty()) {
53 res.insertText = qualifier.data();
54 *res.insertText += '.';
55 *res.insertText += insertText.data();
56 } else {
57 res.insertText = insertText.data();
58 }
59 res.kind = int(CompletionItemKind::Snippet);
60 res.insertTextMode = InsertTextMode::AdjustIndentation;
61 return res;
62}
63
64CompletionItem QQmlLSCompletion::makeSnippet(QUtf8StringView label, QUtf8StringView insertText)
65{
66 return makeSnippet(QByteArray(), label, insertText);
67}
68
69/*!
70\internal
71\brief Compare left and right locations to the position denoted by ctx, see special cases below.
72
73Statements and expressions need to provide different completions depending on where the cursor is.
74For example, lets take following for-statement:
75\badcode
76for (let i = 0; <here> ; ++i) {}
77\endcode
78We want to provide script expression completion (method names, property names, available JS
79variables names, QML objects ids, and so on) at the place denoted by \c{<here>}.
80The question is: how do we know that the cursor is really at \c{<here>}? In the case of the
81for-loop, we can compare the position of the cursor with the first and the second semicolon of the
82for loop.
83
84If the first semicolon does not exist, it has an invalid sourcelocation and the cursor is
85definitively \e{not} at \c{<here>}. Therefore, return false when \c{left} is invalid.
86
87If the second semicolon does not exist, then just ignore it: it might not have been written yet.
88*/
89bool QQmlLSCompletion::betweenLocations(QQmlJS::SourceLocation left,
90 const QQmlLSCompletionPosition &positionInfo,
91 QQmlJS::SourceLocation right) const
92{
93 if (!left.isValid())
94 return false;
95 // note: left.end() == ctx.offset() means that the cursor lies exactly after left
96 if (!(left.end() <= positionInfo.offset()))
97 return false;
98 if (!right.isValid())
99 return true;
100
101 // note: ctx.offset() == right.begin() means that the cursor lies exactly before right
102 return positionInfo.offset() <= right.begin();
103}
104
105/*!
106\internal
107Returns true if ctx denotes an offset lying behind left.end(), and false otherwise.
108*/
109bool QQmlLSCompletion::afterLocation(QQmlJS::SourceLocation left,
110 const QQmlLSCompletionPosition &positionInfo) const
111{
112 return betweenLocations(left, positionInfo, QQmlJS::SourceLocation{});
113}
114
115/*!
116\internal
117Returns true if ctx denotes an offset lying before right.begin(), and false otherwise.
118*/
119bool QQmlLSCompletion::beforeLocation(const QQmlLSCompletionPosition &ctx,
120 QQmlJS::SourceLocation right) const
121{
122 if (!right.isValid())
123 return true;
124
125 // note: ctx.offset() == right.begin() means that the cursor lies exactly before right
126 if (ctx.offset() <= right.begin())
127 return true;
128
129 return false;
130}
131
132bool QQmlLSCompletion::ctxBeforeStatement(const QQmlLSCompletionPosition &positionInfo,
133 const DomItem &parentForContext,
134 FileLocationRegion firstRegion) const
135{
136 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
137 const bool result = beforeLocation(positionInfo, regions[firstRegion]);
138 return result;
139}
140
141void
142QQmlLSCompletion::suggestBindingCompletion(const DomItem &itemAtPosition, BackInsertIterator it) const
143{
144 suggestReachableTypes(itemAtPosition, LocalSymbolsType::AttachedType, CompletionItemKind::Class,
145 it);
146
147 const auto scope = [&]() -> QQmlJSScope::ConstPtr {
148 const DomItem owner = ownerOfQualifiedExpression(itemAtPosition);
149 if (!owner)
150 return itemAtPosition.qmlObject().semanticScope();
151
152 const auto expressionType = QQmlLSUtils::resolveExpressionType(
153 owner, QQmlLSUtils::ResolveActualTypeForFieldMemberExpression);
154
155 // no properties nor signal handlers inside a qualified import
156 if (!expressionType || expressionType->type == QQmlLSUtils::QualifiedModuleIdentifier)
157 return {};
158
159 return expressionType->semanticScope;
160 }();
161
162 if (!scope)
163 return;
164
165 propertyCompletion(scope, nullptr, it);
166 signalHandlerCompletion(scope, nullptr, it);
167}
168
169void QQmlLSCompletion::insideImportCompletionHelper(const DomItem &file,
170 const QQmlLSCompletionPosition &positionInfo,
171 BackInsertIterator it) const
172{
173 // returns completions for import statements, ctx is supposed to be in an import statement
174 const CompletionContextStrings &ctx = positionInfo.cursorPosition;
175 ImportCompletionType importCompletionType = ImportCompletionType::None;
176 QRegularExpression spaceRe(uR"(\s+)"_s);
177 QList<QStringView> linePieces = ctx.preLine().split(spaceRe, Qt::SkipEmptyParts);
178 qsizetype effectiveLength = linePieces.size()
179 + ((!ctx.preLine().isEmpty() && ctx.preLine().last().isSpace()) ? 1 : 0);
180 if (effectiveLength < 2) {
181 CompletionItem comp;
182 comp.label = "import";
183 comp.kind = int(CompletionItemKind::Keyword);
184 it = comp;
185 }
186 if (linePieces.isEmpty() || linePieces.first() != u"import")
187 return;
188 if (effectiveLength == 2) {
189 // the cursor is after the import, possibly in a partial module name
190 importCompletionType = ImportCompletionType::Module;
191 } else if (effectiveLength == 3) {
192 if (linePieces.last() != u"as") {
193 // the cursor is after the module, possibly in a partial version token (or partial as)
194 CompletionItem comp;
195 comp.label = "as";
196 comp.kind = int(CompletionItemKind::Keyword);
197 it = comp;
198 importCompletionType = ImportCompletionType::Version;
199 }
200 }
201 DomItem env = file.environment();
202 if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) {
203 switch (importCompletionType) {
204 case ImportCompletionType::None:
205 break;
206 case ImportCompletionType::Module: {
207 QDuplicateTracker<QString> modulesSeen;
208 for (const QString &uri : envPtr->moduleIndexUris(env)) {
209 QStringView base = ctx.base(); // if we allow spaces we should get rid of them
210 if (uri.startsWith(base)) {
211 QStringList rest = uri.mid(base.size()).split(u'.');
212 if (rest.isEmpty())
213 continue;
214
215 const QString label = rest.first();
216 if (!modulesSeen.hasSeen(label)) {
217 CompletionItem comp;
218 comp.label = label.toUtf8();
219 comp.kind = int(CompletionItemKind::Module);
220 it = comp;
221 }
222 }
223 }
224 break;
225 }
226 case ImportCompletionType::Version:
227 if (ctx.base().isEmpty()) {
228 for (int majorV :
229 envPtr->moduleIndexMajorVersions(env, linePieces.at(1).toString())) {
230 CompletionItem comp;
231 comp.label = QString::number(majorV).toUtf8();
232 comp.kind = int(CompletionItemKind::Constant);
233 it = comp;
234 }
235 } else {
236 bool hasMajorVersion = ctx.base().endsWith(u'.');
237 int majorV = -1;
238 if (hasMajorVersion)
239 majorV = ctx.base().mid(0, ctx.base().size() - 1).toInt(&hasMajorVersion);
240 if (!hasMajorVersion)
241 break;
242 if (std::shared_ptr<ModuleIndex> mIndex =
243 envPtr->moduleIndexWithUri(env, linePieces.at(1).toString(), majorV)) {
244 for (int minorV : mIndex->minorVersions()) {
245 CompletionItem comp;
246 comp.label = QString::number(minorV).toUtf8();
247 comp.kind = int(CompletionItemKind::Constant);
248 it = comp;
249 }
250 }
251 }
252 break;
253 }
254 }
255}
256
257void QQmlLSCompletion::idsCompletions(const DomItem &component, BackInsertIterator it) const
258{
259 qCDebug(QQmlLSCompletionLog) << "adding ids completions";
260 for (const QString &k : component.field(Fields::ids).keys()) {
261 CompletionItem comp;
262 comp.label = k.toUtf8();
263 comp.kind = int(CompletionItemKind::Value);
264 it = comp;
265 }
266}
267
268static bool testScopeSymbol(const QQmlJSScope::ConstPtr &scope, LocalSymbolsTypes options,
269 CompletionItemKind kind)
270{
271 const bool currentIsSingleton = scope->isSingleton();
272 const bool currentIsAttached = !scope->attachedType().isNull();
273 if ((options & LocalSymbolsType::Singleton) && currentIsSingleton) {
274 return true;
275 }
276 if ((options & LocalSymbolsType::AttachedType) && currentIsAttached) {
277 return true;
278 }
279 const bool isObjectType = scope->isReferenceType();
280 if (options & LocalSymbolsType::ObjectType && !currentIsSingleton && isObjectType) {
281 return kind != CompletionItemKind::Constructor || scope->isCreatable();
282 }
283 if (options & LocalSymbolsType::ValueType && !currentIsSingleton && !isObjectType) {
284 return true;
285 }
286 return false;
287}
288
289/*!
290\internal
291Obtain the types reachable from \c{el} as a CompletionItems.
292*/
293void QQmlLSCompletion::suggestReachableTypes(const DomItem &el, LocalSymbolsTypes options,
294 CompletionItemKind kind, BackInsertIterator it) const
295{
296 auto file = el.containingFile().as<QmlFile>();
297 if (!file)
298 return;
299 auto resolver = file->typeResolver();
300 if (!resolver)
301 return;
302
303 const QString requiredQualifiers = QQmlLSUtils::qualifiersFrom(el);
304 const auto keyValueRange = resolver->importedTypes().asKeyValueRange();
305 for (const auto &type : keyValueRange) {
306 // ignore special QQmlJSImporterMarkers
307 const bool isMarkerType = type.first.contains(u"$internal$.")
308 || type.first.contains(u"$anonymous$.") || type.first.contains(u"$module$.");
309 if (isMarkerType || !type.first.startsWith(requiredQualifiers))
310 continue;
311
312 auto &scope = type.second.scope;
313 if (!scope)
314 continue;
315
316 if (!testScopeSymbol(scope, options, kind))
317 continue;
318
319 CompletionItem completion;
320 completion.label = QStringView(type.first).sliced(requiredQualifiers.size()).toUtf8();
321 completion.kind = int(kind);
322 it = completion;
323 }
324}
325
326void QQmlLSCompletion::jsIdentifierCompletion(const QQmlJSScope::ConstPtr &scope,
327 QDuplicateTracker<QString> *usedNames,
328 BackInsertIterator it) const
329{
330 for (const auto &[name, jsIdentifier] : scope->ownJSIdentifiers().asKeyValueRange()) {
331 CompletionItem completion;
332 if (usedNames && usedNames->hasSeen(name)) {
333 continue;
334 }
335 completion.label = name.toUtf8();
336 completion.kind = int(CompletionItemKind::Variable);
337 QString detail = u"has type "_s;
338 if (jsIdentifier.typeName) {
339 if (jsIdentifier.isConst) {
340 detail.append(u"const ");
341 }
342 detail.append(*jsIdentifier.typeName);
343 } else {
344 detail.append(jsIdentifier.isConst ? u"const"_s : u"var"_s);
345 }
346 completion.detail = detail.toUtf8();
347 it = completion;
348 }
349}
350
351void QQmlLSCompletion::methodCompletion(const QQmlJSScope::ConstPtr &scope,
352 QDuplicateTracker<QString> *usedNames,
353 BackInsertIterator it) const
354{
355 // JS functions in current and base scopes
356 for (const auto &[name, method] : scope->methods().asKeyValueRange()) {
357 if (method.access() != QQmlJSMetaMethod::Public)
358 continue;
359 if (usedNames && usedNames->hasSeen(name)) {
360 continue;
361 }
362 CompletionItem completion;
363 completion.label = name.toUtf8();
364 completion.kind = int(CompletionItemKind::Method);
365 it = completion;
366 // TODO: QQmlLSUtils::reachableSymbols seems to be able to do documentation and detail
367 // and co, it should also be done here if possible.
368 }
369}
370
371void QQmlLSCompletion::propertyCompletion(const QQmlJSScope::ConstPtr &scope,
372 QDuplicateTracker<QString> *usedNames,
373 BackInsertIterator it) const
374{
375 for (const auto &[name, property] : scope->properties().asKeyValueRange()) {
376 if (usedNames && usedNames->hasSeen(name)) {
377 continue;
378 }
379 CompletionItem completion;
380 completion.label = name.toUtf8();
381 completion.kind = int(CompletionItemKind::Property);
382 QString detail{ u"has type "_s };
383 if (!property.isWritable())
384 detail.append(u"readonly "_s);
385 detail.append(property.typeName().isEmpty() ? u"var"_s : property.typeName());
386 completion.detail = detail.toUtf8();
387 it = completion;
388 }
389}
390
391void QQmlLSCompletion::enumerationCompletion(const QQmlJSScope::ConstPtr &scope,
392 QDuplicateTracker<QString> *usedNames,
393 BackInsertIterator it) const
394{
395 for (const QQmlJSMetaEnum &enumerator : scope->enumerations()) {
396 if (usedNames && usedNames->hasSeen(enumerator.name())) {
397 continue;
398 }
399 CompletionItem completion;
400 completion.label = enumerator.name().toUtf8();
401 completion.kind = static_cast<int>(CompletionItemKind::Enum);
402 it = completion;
403 }
404}
405
406void QQmlLSCompletion::enumerationValueCompletionHelper(const QStringList &enumeratorKeys,
407 BackInsertIterator it) const
408{
409 for (const QString &enumeratorKey : enumeratorKeys) {
410 CompletionItem completion;
411 completion.label = enumeratorKey.toUtf8();
412 completion.kind = static_cast<int>(CompletionItemKind::EnumMember);
413 it = completion;
414 }
415}
416
417/*!
418\internal
419Creates completion items for enumerationvalues.
420If enumeratorName is a valid enumerator then only do completion for the requested enumerator, and
421otherwise do completion for \b{all other possible} enumerators.
422
423For example:
424```
425id: someItem
426enum Hello { World }
427enum MyEnum { ValueOne, ValueTwo }
428
429// Hello does refer to a enumerator:
430property var a: Hello.<complete only World here>
431
432// someItem does not refer to a enumerator:
433property var b: someItem.<complete World, ValueOne and ValueTwo here>
434```
435*/
436
437void QQmlLSCompletion::enumerationValueCompletion(const QQmlJSScope::ConstPtr &scope,
438 const QString &enumeratorName,
439 BackInsertIterator result) const
440{
441 auto enumerator = scope->enumeration(enumeratorName);
442 if (enumerator.isValid()) {
443 enumerationValueCompletionHelper(enumerator.keys(), result);
444 return;
445 }
446
447 for (const QQmlJSMetaEnum &enumerator : scope->enumerations()) {
448 enumerationValueCompletionHelper(enumerator.keys(), result);
449 }
450}
451
452/*!
453\internal
454Calls F on all JavaScript-parents of scope. For example, you can use this method to
455collect all the JavaScript Identifiers from following code:
456```
457{ // this block statement contains only 'x'
458 let x = 3;
459 { // this block statement contains only 'y', and 'x' has to be retrieved via its parent.
460 let y = 4;
461 }
462}
463```
464*/
465template<typename F>
466void collectFromAllJavaScriptParents(const F &&f, const QQmlJSScope::ConstPtr &scope)
467{
468 for (QQmlJSScope::ConstPtr current = scope; current; current = current->parentScope()) {
469 f(current);
470 if (current->scopeType() == QQmlSA::ScopeType::QMLScope)
471 return;
472 }
473}
474
475/*!
476\internal
477Suggest enumerations (if applicable) and enumeration values from \c scope, for example \c
478Asynchronous from the \c CompilationMode enum:
479
480\qml
481property var xxx: Component.Asynchronous // Component contains the \c CompilationMode enum
482property var xxx2: CompilationMode.Asynchronous
483\endqml
484*/
485void QQmlLSCompletion::suggestEnumerationsAndEnumerationValues(
486 const QQmlJSScope::ConstPtr &scope, const QString &enumName,
487 QDuplicateTracker<QString> &usedNames, BackInsertIterator result) const
488{
489 enumerationValueCompletion(scope, enumName, result);
490
491 // skip enumeration types if already inside an enumeration type
492 if (auto enumerator = scope->enumeration(enumName); !enumerator.isValid()) {
493 enumerationCompletion(scope, &usedNames, result);
494 }
495}
496
497/*!
498\internal
499
500Returns the owner of a qualified expression for further resolving, for example:
5011. \c owner from the \c member ScriptExpression in \c {owner.member}. This happens when completion
502is requested on \c member.
5032. \c owner from the ScriptBinaryExpression \c {owner.member}. This happens when completion is
504requested on the dot between \c owner and \c member.
5053. An empty DomItem otherwise.
506*/
507DomItem QQmlLSCompletion::ownerOfQualifiedExpression(const DomItem &qualifiedExpression) const
508{
509 // note: there is an edge case, where the user asks for completion right after the dot
510 // of some qualified expression like `root.hello`. In this case, scriptIdentifier is actually
511 // the BinaryExpression instead of the left-hand-side that has not be written down yet.
512 const bool askForCompletionOnDot = QQmlLSUtils::isFieldMemberExpression(qualifiedExpression);
513 const bool hasQualifier =
514 QQmlLSUtils::isFieldMemberAccess(qualifiedExpression) || askForCompletionOnDot;
515
516 if (!hasQualifier)
517 return {};
518
519 const DomItem owner =
520 (askForCompletionOnDot ? qualifiedExpression : qualifiedExpression.directParent())
521 .field(Fields::left);
522 return owner;
523}
524
525/*!
526\internal
527Generate autocompletions for JS expressions, suggest possible properties, methods, etc.
528
529If scriptIdentifier is inside a Field Member Expression, like \c{onCompleted} in
530\c{Component.onCompleted} for example, then this method will only suggest properties, methods, etc
531from the correct type. For the previous example that would be properties, methods, etc. from the
532Component attached type.
533*/
534void QQmlLSCompletion::suggestJSExpressionCompletion(const DomItem &scriptIdentifier,
535 BackInsertIterator result) const
536{
537 QDuplicateTracker<QString> usedNames;
538 QQmlJSScope::ConstPtr nearestScope;
539
540 const DomItem owner = ownerOfQualifiedExpression(scriptIdentifier);
541
542 if (!owner) {
543 for (QUtf8StringView view : std::array<QUtf8StringView, 3>{ "null", "false", "true" }) {
544 CompletionItem completion;
545 completion.label = view.data();
546 completion.kind = int(CompletionItemKind::Value);
547 result = completion;
548 }
549 idsCompletions(scriptIdentifier.component(), result);
550 suggestReachableTypes(scriptIdentifier,
551 LocalSymbolsType::Singleton | LocalSymbolsType::AttachedType,
552 CompletionItemKind::Class, result);
553
554 auto scope = scriptIdentifier.nearestSemanticScope();
555 if (!scope)
556 return;
557 nearestScope = scope;
558
559 enumerationCompletion(nearestScope, &usedNames, result);
560 } else {
561 auto ownerExpressionType = QQmlLSUtils::resolveExpressionType(
562 owner, QQmlLSUtils::ResolveActualTypeForFieldMemberExpression);
563 if (!ownerExpressionType || !ownerExpressionType->semanticScope)
564 return;
565 nearestScope = ownerExpressionType->semanticScope;
566
567 switch (ownerExpressionType->type) {
568 case QQmlLSUtils::EnumeratorValueIdentifier:
569 return;
570 case QQmlLSUtils::EnumeratorIdentifier:
571 suggestEnumerationsAndEnumerationValues(nearestScope, *ownerExpressionType->name,
572 usedNames, result);
573 return;
574 case QQmlLSUtils::QmlComponentIdentifier:
575 // Suggest members of the attached type, for example suggest `progress` in
576 // `property real p: Component.progress`.
577 if (QQmlJSScope::ConstPtr attachedType =
578 ownerExpressionType->semanticScope->attachedType()) {
579 methodCompletion(attachedType, &usedNames, result);
580 propertyCompletion(attachedType, &usedNames, result);
581 suggestEnumerationsAndEnumerationValues(
582 attachedType, *ownerExpressionType->name, usedNames, result);
583 }
584 Q_FALLTHROUGH();
585 case QQmlLSUtils::SingletonIdentifier:
586 if (ownerExpressionType->name)
587 suggestEnumerationsAndEnumerationValues(nearestScope, *ownerExpressionType->name,
588 usedNames, result);
589 break;
590 default:
591 break;
592 }
593 }
594
595 Q_ASSERT(nearestScope);
596
597 methodCompletion(nearestScope, &usedNames, result);
598 propertyCompletion(nearestScope, &usedNames, result);
599
600 if (!owner) {
601 // collect all of the stuff from parents
602 collectFromAllJavaScriptParents(
603 [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) {
604 jsIdentifierCompletion(scope, &usedNames, result);
605 },
606 nearestScope);
607 collectFromAllJavaScriptParents(
608 [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) {
609 methodCompletion(scope, &usedNames, result);
610 },
611 nearestScope);
612 collectFromAllJavaScriptParents(
613 [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) {
614 propertyCompletion(scope, &usedNames, result);
615 },
616 nearestScope);
617
618 auto file = scriptIdentifier.containingFile().as<QmlFile>();
619 if (!file)
620 return;
621 auto resolver = file->typeResolver();
622 if (!resolver)
623 return;
624
625 const auto globals = resolver->jsGlobalObject();
626 methodCompletion(globals, &usedNames, result);
627 propertyCompletion(globals, &usedNames, result);
628 }
629}
630
631static const QQmlJSScope *resolve(const QQmlJSScope *current, const QStringList &names)
632{
633 for (const QString &name : names) {
634 if (auto property = current->property(name); property.isValid()) {
635 if (auto propertyType = property.type().get()) {
636 current = propertyType;
637 continue;
638 }
639 }
640 return {};
641 }
642 return current;
643}
644
645bool QQmlLSCompletion::cursorInFrontOfItem(const DomItem &parentForContext,
646 const QQmlLSCompletionPosition &positionInfo)
647{
648 auto fileLocations = FileLocations::treeOf(parentForContext)->info().fullRegion;
649 return positionInfo.offset() <= fileLocations.begin();
650}
651
652bool QQmlLSCompletion::cursorAfterColon(const DomItem &currentItem,
653 const QQmlLSCompletionPosition &positionInfo)
654{
655 auto location = FileLocations::treeOf(currentItem)->info();
656 auto region = location.regions.constFind(ColonTokenRegion);
657
658 if (region == location.regions.constEnd())
659 return false;
660
661 if (region.value().isValid() && region.value().begin() < positionInfo.offset()) {
662 return true;
663 }
664 return false;
665}
666
667/*!
668\internal
669\brief Mapping from pragma names to allowed pragma values.
670
671This mapping of pragma names to pragma values is not complete. In fact, it only contains the
672pragma names and values that one should see autocompletion for.
673Some pragmas like FunctionSignatureBehavior or Strict or the Reference/Value of ValueTypeBehavior,
674for example, should currently not be proposed as completion items by qmlls.
675
676An empty QList-value in the QMap means that the pragma does not accept pragma values.
677*/
678static const QMap<QString, QList<QString>> valuesForPragmas{
679 { u"ComponentBehavior"_s, { u"Unbound"_s, u"Bound"_s } },
680 { u"NativeMethodBehavior"_s, { u"AcceptThisObject"_s, u"RejectThisObject"_s } },
681 { u"ListPropertyAssignBehavior"_s, { u"Append"_s, u"Replace"_s, u"ReplaceIfNotDefault"_s } },
682 { u"Singleton"_s, {} },
683 { u"ValueTypeBehavior"_s, { u"Addressable"_s, u"Inaddressable"_s } },
684};
685
686void QQmlLSCompletion::insidePragmaCompletion(QQmlJS::Dom::DomItem currentItem,
687 const QQmlLSCompletionPosition &positionInfo,
688 BackInsertIterator result) const
689{
690 if (cursorAfterColon(currentItem, positionInfo)) {
691 const QString name = currentItem.field(Fields::name).value().toString();
692 auto values = valuesForPragmas.constFind(name);
693 if (values == valuesForPragmas.constEnd())
694 return;
695
696 for (const auto &value : *values) {
697 CompletionItem comp;
698 comp.label = value.toUtf8();
699 comp.kind = static_cast<int>(CompletionItemKind::Value);
700 result = comp;
701 }
702 return;
703 }
704
705 for (const auto &pragma : valuesForPragmas.asKeyValueRange()) {
706 CompletionItem comp;
707 comp.label = pragma.first.toUtf8();
708 if (!pragma.second.isEmpty()) {
709 comp.insertText = QString(pragma.first).append(u": ").toUtf8();
710 }
711 comp.kind = static_cast<int>(CompletionItemKind::Value);
712 result = comp;
713 }
714}
715
716void QQmlLSCompletion::insideQmlObjectCompletion(const DomItem &parentForContext,
717 const QQmlLSCompletionPosition &positionInfo,
718 BackInsertIterator result) const
719{
720
721 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
722
723 const QQmlJS::SourceLocation leftBrace = regions[LeftBraceRegion];
724 const QQmlJS::SourceLocation rightBrace = regions[RightBraceRegion];
725
726 if (beforeLocation(positionInfo, leftBrace)) {
727 LocalSymbolsTypes options;
728 options.setFlag(LocalSymbolsType::ObjectType);
729 suggestReachableTypes(positionInfo.itemAtPosition, options, CompletionItemKind::Constructor,
730 result);
731 if (parentForContext.directParent().internalKind() == DomType::Binding)
732 suggestSnippetsForRightHandSideOfBinding(positionInfo.itemAtPosition, result);
733 else
734 suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result);
735
736 if (QQmlLSUtils::isFieldMemberExpression(positionInfo.itemAtPosition)) {
737 /*!
738 \internal
739 In the case that a missing identifier is followed by an assignment to the default
740 property, the parser will create a QmlObject out of both binding and default
741 binding. For example, in \code property int x: root. Item {} \endcode the parser will
742 create one binding containing one QmlObject of type `root.Item`, instead of two
743 bindings (one for `x` and one for the default property). For this special case, if
744 completion is requested inside `root.Item`, then try to also suggest JS expressions.
745
746 Note: suggestJSExpressionCompletion() will suggest nothing if the
747 fieldMemberExpression starts with the name of a qualified module or a filename, so
748 this only adds invalid suggestions in the case that there is something shadowing the
749 qualified module name or filename, like a property name for example.
750
751 Note 2: This does not happen for field member accesses. For example, in
752 \code
753 property int x: root.x
754 Item {}
755 \endcode
756 The parser will create both bindings correctly.
757 */
758 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
759 }
760 return;
761 }
762
763 if (betweenLocations(leftBrace, positionInfo, rightBrace)) {
764 // default/final/required/readonly property completion
765 constexpr static std::array completions {
766 ""_L1, "default "_L1, "final "_L1, "required "_L1, "readonly "_L1, "default final "_L1,
767 "default required "_L1, "final required "_L1, "final readonly "_L1,
768 "default final required "_L1 };
769 for (QLatin1StringView view : completions) {
770 // readonly properties require an initializer
771 if (view != QUtf8StringView("readonly ")) {
772 result = makeSnippet(
773 QByteArray(view.data()).append("property type name;"),
774 QByteArray(view.data()).append("property ${1:type} ${0:name};"));
775 }
776
777 result = makeSnippet(
778 QByteArray(view.data()).append("property type name: value;"),
779 QByteArray(view.data()).append("property ${1:type} ${2:name}: ${0:value};"));
780 }
781
782 // signal
783 result = makeSnippet("signal name(arg1:type1, ...)", "signal ${1:name}($0)");
784
785 // signal without parameters
786 result = makeSnippet("signal name;", "signal ${0:name};");
787
788 // make already existing property required
789 result = makeSnippet("required name;", "required ${0:name};");
790
791 // function
792 result = makeSnippet("function name(args...): returnType { statements...}",
793 "function ${1:name}($2): ${3:returnType} {\n\t$0\n}");
794
795 // enum
796 result = makeSnippet("enum name { Values...}", "enum ${1:name} {\n\t${0:values}\n}");
797
798 // inline component
799 result = makeSnippet("component Name: BaseType { ... }",
800 "component ${1:name}: ${2:baseType} {\n\t$0\n}");
801
802 suggestBindingCompletion(positionInfo.itemAtPosition, result);
803
804 // add Qml Types for default binding
805 const DomItem containingFile = parentForContext.containingFile();
806 suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType,
807 CompletionItemKind::Constructor, result);
808 suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result);
809 return;
810 }
811}
812
813void QQmlLSCompletion::insidePropertyDefinitionCompletion(
814 const DomItem &currentItem, const QQmlLSCompletionPosition &positionInfo,
815 BackInsertIterator result) const
816{
817 auto info = FileLocations::treeOf(currentItem)->info();
818 const QQmlJS::SourceLocation propertyKeyword = info.regions[PropertyKeywordRegion];
819
820 // do completions for the keywords
821 if (positionInfo.offset() < propertyKeyword.end()) {
822 const QQmlJS::SourceLocation readonlyKeyword = info.regions[ReadonlyKeywordRegion];
823 const QQmlJS::SourceLocation defaultKeyword = info.regions[DefaultKeywordRegion];
824 const QQmlJS::SourceLocation requiredKeyword = info.regions[RequiredKeywordRegion];
825 const QQmlJS::SourceLocation finalKeyword = info.regions[FinalKeywordRegion];
826
827 bool completeReadonly = true;
828 bool completeRequired = true;
829 bool completeDefault = true;
830 bool completeFinal = true;
831
832 // if there is already a readonly keyword before the cursor: do not auto complete it again
833 if (readonlyKeyword.isValid() && readonlyKeyword.begin() < positionInfo.offset()) {
834 completeReadonly = false;
835 // also, required keywords do not like readonly keywords
836 completeRequired = false;
837 }
838
839 // same for required
840 if (requiredKeyword.isValid() && requiredKeyword.begin() < positionInfo.offset()) {
841 completeRequired = false;
842 // also, required keywords do not like readonly keywords
843 completeReadonly = false;
844 }
845
846 // same for default
847 if (defaultKeyword.isValid() && defaultKeyword.begin() < positionInfo.offset()) {
848 completeDefault = false;
849 }
850
851 // same for final
852 if (finalKeyword.isValid() && finalKeyword.begin() < positionInfo.offset())
853 completeFinal = false;
854
855 auto addCompletionKeyword = [&result](QUtf8StringView view, bool complete) {
856 if (!complete)
857 return;
858 CompletionItem item;
859 item.label = view.data();
860 item.kind = int(CompletionItemKind::Keyword);
861 result = item;
862 };
863 addCompletionKeyword(u8"readonly", completeReadonly);
864 addCompletionKeyword(u8"required", completeRequired);
865 addCompletionKeyword(u8"default", completeDefault);
866 addCompletionKeyword(u8"final", completeFinal);
867 addCompletionKeyword(u8"property", true);
868
869 return;
870 }
871
872 const QQmlJS::SourceLocation propertyIdentifier = info.regions[IdentifierRegion];
873 if (propertyKeyword.end() <= positionInfo.offset()
874 && positionInfo.offset() < propertyIdentifier.begin()) {
875 suggestReachableTypes(currentItem,
876 LocalSymbolsType::ObjectType | LocalSymbolsType::ValueType,
877 CompletionItemKind::Class, result);
878 }
879 // do not autocomplete the rest
880 return;
881}
882
883void QQmlLSCompletion::insideBindingCompletion(const DomItem &currentItem,
884 const QQmlLSCompletionPosition &positionInfo,
885 BackInsertIterator result) const
886{
887 const DomItem containingBinding = currentItem.filterUp(
888 [](DomType type, const QQmlJS::Dom::DomItem &) { return type == DomType::Binding; },
889 FilterUpOptions::ReturnOuter);
890
891 // do scriptidentifiercompletion after the ':' of a binding
892 if (cursorAfterColon(containingBinding, positionInfo)) {
893 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
894
895 if (auto type = QQmlLSUtils::resolveExpressionType(currentItem,
896 QQmlLSUtils::ResolveOwnerType)) {
897 const QStringList names = currentItem.field(Fields::name).toString().split(u'.');
898 const QQmlJSScope *current = resolve(type->semanticScope.get(), names);
899 // add type names when binding to an object type or a property with var type
900 if (!current || current->accessSemantics() == QQmlSA::AccessSemantics::Reference) {
901 LocalSymbolsTypes options;
902 options.setFlag(LocalSymbolsType::ObjectType);
903 suggestReachableTypes(positionInfo.itemAtPosition, options,
904 CompletionItemKind::Constructor, result);
905 suggestSnippetsForRightHandSideOfBinding(positionInfo.itemAtPosition, result);
906 }
907 }
908 return;
909 }
910
911 // ignore the binding if asking for completion in front of the binding
912 if (cursorInFrontOfItem(containingBinding, positionInfo)) {
913 insideQmlObjectCompletion(currentItem.containingObject(), positionInfo, result);
914 return;
915 }
916
917 const DomItem containingObject = currentItem.qmlObject();
918
919 suggestBindingCompletion(positionInfo.itemAtPosition, result);
920
921 // add Qml Types for default binding
922 suggestReachableTypes(positionInfo.itemAtPosition, LocalSymbolsType::ObjectType,
923 CompletionItemKind::Constructor, result);
924 suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result);
925}
926
927void QQmlLSCompletion::insideImportCompletion(const DomItem &currentItem,
928 const QQmlLSCompletionPosition &positionInfo,
929 BackInsertIterator result) const
930{
931 const DomItem containingFile = currentItem.containingFile();
932 insideImportCompletionHelper(containingFile, positionInfo, result);
933
934 // when in front of the import statement: propose types for root Qml Object completion
935 if (cursorInFrontOfItem(currentItem, positionInfo)) {
936 suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType,
937 CompletionItemKind::Constructor, result);
938 }
939}
940
941void QQmlLSCompletion::insideQmlFileCompletion(const DomItem &currentItem,
942 const QQmlLSCompletionPosition &positionInfo,
943 BackInsertIterator result) const
944{
945 const DomItem containingFile = currentItem.containingFile();
946 // completions for code outside the root Qml Object
947 // global completions
948 if (positionInfo.cursorPosition.atLineStart()) {
949 if (positionInfo.cursorPosition.base().isEmpty()) {
950 for (const QStringView &s : std::array<QStringView, 2>({ u"pragma", u"import" })) {
951 CompletionItem comp;
952 comp.label = s.toUtf8();
953 comp.kind = int(CompletionItemKind::Keyword);
954 result = comp;
955 }
956 }
957 }
958 // Types for root Qml Object completion
959 suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType,
960 CompletionItemKind::Constructor, result);
961}
962
963/*!
964\internal
965Generate the snippets for let, var and const variable declarations.
966*/
967void QQmlLSCompletion::suggestVariableDeclarationStatementCompletion(
968 BackInsertIterator result, AppendOption option) const
969{
970 // let/var/const statement
971 for (auto view : std::array<QUtf8StringView, 3>{ "let", "var", "const" }) {
972 auto snippet = makeSnippet(QByteArray(view.data()).append(" variable = value"),
973 QByteArray(view.data()).append(" ${1:variable} = $0"));
974 if (option == AppendSemicolon) {
975 snippet.insertText->append(";");
976 snippet.label.append(";");
977 }
978 result = snippet;
979 }
980}
981
982/*!
983\internal
984Generate the snippets for case and default statements.
985*/
986void QQmlLSCompletion::suggestCaseAndDefaultStatementCompletion(BackInsertIterator result) const
987{
988 // case snippet
989 result = makeSnippet("case value: statements...", "case ${1:value}:\n\t$0");
990 // case + brackets snippet
991 result = makeSnippet("case value: { statements... }", "case ${1:value}: {\n\t$0\n}");
992
993 // default snippet
994 result = makeSnippet("default: statements...", "default:\n\t$0");
995 // default + brackets snippet
996 result = makeSnippet("default: { statements... }", "default: {\n\t$0\n}");
997}
998
999/*!
1000\internal
1001Break and continue can be inserted only in following situations:
1002\list
1003 \li Break and continue inside a loop.
1004 \li Break inside a (nested) LabelledStatement
1005 \li Break inside a (nested) SwitchStatement
1006\endlist
1007*/
1008void QQmlLSCompletion::suggestContinueAndBreakStatementIfNeeded(const DomItem &itemAtPosition,
1009 BackInsertIterator result) const
1010{
1011 bool alreadyInLabel = false;
1012 bool alreadyInSwitch = false;
1013 for (DomItem current = itemAtPosition; current; current = current.directParent()) {
1014 switch (current.internalKind()) {
1015 case DomType::ScriptExpression:
1016 // reached end of script expression
1017 return;
1018
1019 case DomType::ScriptForStatement:
1020 case DomType::ScriptForEachStatement:
1021 case DomType::ScriptWhileStatement:
1022 case DomType::ScriptDoWhileStatement: {
1023 CompletionItem continueKeyword;
1024 continueKeyword.label = "continue";
1025 continueKeyword.kind = int(CompletionItemKind::Keyword);
1026 result = continueKeyword;
1027
1028 // do not add break twice
1029 if (!alreadyInSwitch && !alreadyInLabel) {
1030 CompletionItem breakKeyword;
1031 breakKeyword.label = "break";
1032 breakKeyword.kind = int(CompletionItemKind::Keyword);
1033 result = breakKeyword;
1034 }
1035 // early exit: cannot suggest more completions
1036 return;
1037 }
1038 case DomType::ScriptSwitchStatement: {
1039 // check if break was already inserted
1040 if (alreadyInSwitch || alreadyInLabel)
1041 break;
1042 alreadyInSwitch = true;
1043
1044 CompletionItem breakKeyword;
1045 breakKeyword.label = "break";
1046 breakKeyword.kind = int(CompletionItemKind::Keyword);
1047 result = breakKeyword;
1048 break;
1049 }
1050 case DomType::ScriptLabelledStatement: {
1051 // check if break was already inserted because of switch or loop
1052 if (alreadyInSwitch || alreadyInLabel)
1053 break;
1054 alreadyInLabel = true;
1055
1056 CompletionItem breakKeyword;
1057 breakKeyword.label = "break";
1058 breakKeyword.kind = int(CompletionItemKind::Keyword);
1059 result = breakKeyword;
1060 break;
1061 }
1062 default:
1063 break;
1064 }
1065 }
1066}
1067
1068/*!
1069\internal
1070Generates snippets or keywords for all possible JS statements where it makes sense. To use whenever
1071any JS statement can be expected, but when no JS statement is there yet.
1072
1073Only generates JS expression completions when itemAtPosition is a qualified name.
1074
1075Here is a list of statements that do \e{not} get any snippets:
1076\list
1077 \li BlockStatement does not need a code snippet, editors automatically include the closing
1078bracket anyway. \li EmptyStatement completion would only generate a single \c{;} \li
1079ExpressionStatement completion cannot generate any snippet, only identifiers \li WithStatement
1080completion is not recommended: qmllint will warn about usage of with statements \li
1081LabelledStatement completion might need to propose labels (TODO?) \li DebuggerStatement completion
1082does not strike as being very useful \endlist
1083*/
1084void QQmlLSCompletion::suggestJSStatementCompletion(const DomItem &itemAtPosition,
1085 BackInsertIterator result) const
1086{
1087 suggestJSExpressionCompletion(itemAtPosition, result);
1088
1089 if (QQmlLSUtils::isFieldMemberAccess(itemAtPosition)
1090 || QQmlLSUtils::isFieldMemberExpression(itemAtPosition))
1091 return;
1092
1093 // expression statements
1094 suggestVariableDeclarationStatementCompletion(result);
1095 // block statement
1096 result = makeSnippet("{ statements... }", "{\n\t$0\n}");
1097
1098 // if + brackets statement
1099 result = makeSnippet("if (condition) { statements }", "if ($1) {\n\t$0\n}");
1100
1101 // do statement
1102 result = makeSnippet("do { statements } while (condition);", "do {\n\t$1\n} while ($0);");
1103
1104 // while + brackets statement
1105 result = makeSnippet("while (condition) { statements...}", "while ($1) {\n\t$0\n}");
1106
1107 // for + brackets loop statement
1108 result = makeSnippet("for (initializer; condition; increment) { statements... }",
1109 "for ($1;$2;$3) {\n\t$0\n}");
1110
1111 // for ... in + brackets loop statement
1112 result = makeSnippet("for (property in object) { statements... }", "for ($1 in $2) {\n\t$0\n}");
1113
1114 // for ... of + brackets loop statement
1115 result = makeSnippet("for (element of array) { statements... }", "for ($1 of $2) {\n\t$0\n}");
1116
1117 // try + catch statement
1118 result = makeSnippet("try { statements... } catch(error) { statements... }",
1119 "try {\n\t$1\n} catch($2) {\n\t$0\n}");
1120
1121 // try + finally statement
1122 result = makeSnippet("try { statements... } finally { statements... }",
1123 "try {\n\t$1\n} finally {\n\t$0\n}");
1124
1125 // try + catch + finally statement
1126 result = makeSnippet(
1127 "try { statements... } catch(error) { statements... } finally { statements... }",
1128 "try {\n\t$1\n} catch($2) {\n\t$3\n} finally {\n\t$0\n}");
1129
1130 // one can always assume that JS code in QML is inside a function, so always propose `return`
1131 for (auto &&view : { "return"_ba, "throw"_ba }) {
1132 CompletionItem item;
1133 item.label = std::move(view);
1134 item.kind = int(CompletionItemKind::Keyword);
1135 result = item;
1136 }
1137
1138 // rules for case+default statements:
1139 // 1) when inside a CaseBlock, or
1140 // 2) inside a CaseClause, as an (non-nested) element of the CaseClause statementlist.
1141 // 3) inside a DefaultClause, as an (non-nested) element of the DefaultClause statementlist,
1142 //
1143 // switch (x) {
1144 // // (1)
1145 // case 1:
1146 // myProperty = 5;
1147 // // (2) -> could be another statement of current case, but also a new case or default!
1148 // default:
1149 // myProperty = 5;
1150 // // (3) -> could be another statement of current default, but also a new case or default!
1151 // }
1152 const DomType currentKind = itemAtPosition.internalKind();
1153 const DomType parentKind = itemAtPosition.directParent().internalKind();
1154 if (currentKind == DomType::ScriptCaseBlock || currentKind == DomType::ScriptCaseClause
1155 || currentKind == DomType::ScriptDefaultClause
1156 || (currentKind == DomType::List
1157 && (parentKind == DomType::ScriptCaseClause
1158 || parentKind == DomType::ScriptDefaultClause))) {
1159 suggestCaseAndDefaultStatementCompletion(result);
1160 }
1161 suggestContinueAndBreakStatementIfNeeded(itemAtPosition, result);
1162}
1163
1164void QQmlLSCompletion::insideForStatementCompletion(const DomItem &parentForContext,
1165 const QQmlLSCompletionPosition &positionInfo,
1166 BackInsertIterator result) const
1167{
1168 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1169
1170 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1171 const QQmlJS::SourceLocation firstSemicolon = regions[FirstSemicolonTokenRegion];
1172 const QQmlJS::SourceLocation secondSemicolon = regions[SecondSemicolonRegion];
1173 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1174
1175 if (betweenLocations(leftParenthesis, positionInfo, firstSemicolon)) {
1176 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1177 suggestVariableDeclarationStatementCompletion(result,
1178 AppendOption::AppendNothing);
1179 return;
1180 }
1181 if (betweenLocations(firstSemicolon, positionInfo, secondSemicolon)
1182 || betweenLocations(secondSemicolon, positionInfo, rightParenthesis)) {
1183 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1184 return;
1185 }
1186
1187 if (afterLocation(rightParenthesis, positionInfo)) {
1188 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1189 return;
1190 }
1191}
1192
1193void QQmlLSCompletion::insideScriptLiteralCompletion(const DomItem &currentItem,
1194 const QQmlLSCompletionPosition &positionInfo,
1195 BackInsertIterator result) const
1196{
1197 Q_UNUSED(currentItem);
1198 if (positionInfo.cursorPosition.base().isEmpty()) {
1199 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1200 return;
1201 }
1202}
1203
1204void QQmlLSCompletion::insideCallExpression(const DomItem &currentItem,
1205 const QQmlLSCompletionPosition &positionInfo,
1206 BackInsertIterator result) const
1207{
1208 const auto regions = FileLocations::treeOf(currentItem)->info().regions;
1209 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1210 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1211 if (beforeLocation(positionInfo, leftParenthesis)) {
1212 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1213 return;
1214 }
1215 if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) {
1216 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1217 return;
1218 }
1219}
1220
1221void QQmlLSCompletion::insideIfStatement(const DomItem &currentItem,
1222 const QQmlLSCompletionPosition &positionInfo,
1223 BackInsertIterator result) const
1224{
1225 const auto regions = FileLocations::treeOf(currentItem)->info().regions;
1226 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1227 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1228 const QQmlJS::SourceLocation elseKeyword = regions[ElseKeywordRegion];
1229
1230 if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) {
1231 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1232 return;
1233 }
1234 if (betweenLocations(rightParenthesis, positionInfo, elseKeyword)) {
1235 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1236 return;
1237 }
1238 if (afterLocation(elseKeyword, positionInfo)) {
1239 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1240 return;
1241 }
1242}
1243
1244void QQmlLSCompletion::insideReturnStatement(const DomItem &currentItem,
1245 const QQmlLSCompletionPosition &positionInfo,
1246 BackInsertIterator result) const
1247{
1248 const auto regions = FileLocations::treeOf(currentItem)->info().regions;
1249 const QQmlJS::SourceLocation returnKeyword = regions[ReturnKeywordRegion];
1250
1251 if (afterLocation(returnKeyword, positionInfo)) {
1252 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1253 return;
1254 }
1255}
1256
1257void QQmlLSCompletion::insideWhileStatement(const DomItem &currentItem,
1258 const QQmlLSCompletionPosition &positionInfo,
1259 BackInsertIterator result) const
1260{
1261 const auto regions = FileLocations::treeOf(currentItem)->info().regions;
1262 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1263 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1264
1265 if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) {
1266 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1267 return;
1268 }
1269 if (afterLocation(rightParenthesis, positionInfo)) {
1270 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1271 return;
1272 }
1273}
1274
1275void QQmlLSCompletion::insideDoWhileStatement(const DomItem &parentForContext,
1276 const QQmlLSCompletionPosition &positionInfo,
1277 BackInsertIterator result) const
1278{
1279 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1280 const QQmlJS::SourceLocation doKeyword = regions[DoKeywordRegion];
1281 const QQmlJS::SourceLocation whileKeyword = regions[WhileKeywordRegion];
1282 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1283 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1284
1285 if (betweenLocations(doKeyword, positionInfo, whileKeyword)) {
1286 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1287 return;
1288 }
1289 if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) {
1290 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1291 return;
1292 }
1293}
1294
1295void QQmlLSCompletion::insideForEachStatement(const DomItem &parentForContext,
1296 const QQmlLSCompletionPosition &positionInfo,
1297 BackInsertIterator result) const
1298{
1299 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1300
1301 const QQmlJS::SourceLocation inOf = regions[InOfTokenRegion];
1302 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1303 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1304
1305 if (betweenLocations(leftParenthesis, positionInfo, inOf)) {
1306 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1307 suggestVariableDeclarationStatementCompletion(result);
1308 return;
1309 }
1310 if (betweenLocations(inOf, positionInfo, rightParenthesis)) {
1311 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1312 return;
1313 }
1314
1315 if (afterLocation(rightParenthesis, positionInfo)) {
1316 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1317 return;
1318 }
1319}
1320
1321void QQmlLSCompletion::insideSwitchStatement(const DomItem &parentForContext,
1322 const QQmlLSCompletionPosition positionInfo,
1323 BackInsertIterator result) const
1324{
1325 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1326
1327 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1328 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1329
1330 if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) {
1331 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1332 return;
1333 }
1334}
1335
1336void QQmlLSCompletion::insideCaseClause(const DomItem &parentForContext,
1337 const QQmlLSCompletionPosition &positionInfo,
1338 BackInsertIterator result) const
1339{
1340 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1341
1342 const QQmlJS::SourceLocation caseKeyword = regions[CaseKeywordRegion];
1343 const QQmlJS::SourceLocation colonToken = regions[ColonTokenRegion];
1344
1345 if (betweenLocations(caseKeyword, positionInfo, colonToken)) {
1346 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1347 return;
1348 }
1349 if (afterLocation(colonToken, positionInfo)) {
1350 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1351 return;
1352 }
1353
1354}
1355
1356/*!
1357\internal
1358Checks if a case or default clause does happen before ctx in the code.
1359*/
1360bool QQmlLSCompletion::isCaseOrDefaultBeforeCtx(const DomItem &currentClause,
1361 const QQmlLSCompletionPosition &positionInfo,
1362 FileLocationRegion keywordRegion) const
1363{
1364 Q_ASSERT(keywordRegion == QQmlJS::Dom::CaseKeywordRegion
1365 || keywordRegion == QQmlJS::Dom::DefaultKeywordRegion);
1366
1367 if (!currentClause)
1368 return false;
1369
1370 const auto token = FileLocations::treeOf(currentClause)->info().regions[keywordRegion];
1371 if (afterLocation(token, positionInfo))
1372 return true;
1373
1374 return false;
1375}
1376
1377/*!
1378\internal
1379
1380Search for a `case ...:` or a `default: ` clause happening before ctx, and return the
1381corresponding DomItem of type DomType::CaseClauses or DomType::DefaultClause.
1382
1383Return an empty DomItem if neither case nor default was found.
1384*/
1385DomItem
1386QQmlLSCompletion::previousCaseOfCaseBlock(const DomItem &parentForContext,
1387 const QQmlLSCompletionPosition &positionInfo) const
1388{
1389 const DomItem caseClauses = parentForContext.field(Fields::caseClauses);
1390 for (int i = 0; i < caseClauses.indexes(); ++i) {
1391 const DomItem currentClause = caseClauses.index(i);
1392 if (isCaseOrDefaultBeforeCtx(currentClause, positionInfo, QQmlJS::Dom::CaseKeywordRegion)) {
1393 return currentClause;
1394 }
1395 }
1396
1397 const DomItem defaultClause = parentForContext.field(Fields::defaultClause);
1398 if (isCaseOrDefaultBeforeCtx(defaultClause, positionInfo, QQmlJS::Dom::DefaultKeywordRegion))
1399 return parentForContext.field(Fields::defaultClause);
1400
1401 const DomItem moreCaseClauses = parentForContext.field(Fields::moreCaseClauses);
1402 for (int i = 0; i < moreCaseClauses.indexes(); ++i) {
1403 const DomItem currentClause = moreCaseClauses.index(i);
1404 if (isCaseOrDefaultBeforeCtx(currentClause, positionInfo, QQmlJS::Dom::CaseKeywordRegion)) {
1405 return currentClause;
1406 }
1407 }
1408
1409 return {};
1410}
1411
1412void QQmlLSCompletion::insideCaseBlock(const DomItem &parentForContext,
1413 const QQmlLSCompletionPosition &positionInfo,
1414 BackInsertIterator result) const
1415{
1416 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1417
1418 const QQmlJS::SourceLocation leftBrace = regions[LeftBraceRegion];
1419 const QQmlJS::SourceLocation rightBrace = regions[RightBraceRegion];
1420
1421 if (!betweenLocations(leftBrace, positionInfo, rightBrace))
1422 return;
1423
1424 // TODO: looks fishy
1425 // if there is a previous case or default clause, you can still add statements to it
1426 if (const auto previousCase = previousCaseOfCaseBlock(parentForContext, positionInfo)) {
1427 suggestJSStatementCompletion(previousCase, result);
1428 return;
1429 }
1430
1431 // otherwise, only complete case and default
1432 suggestCaseAndDefaultStatementCompletion(result);
1433}
1434
1435void QQmlLSCompletion::insideDefaultClause(const DomItem &parentForContext,
1436 const QQmlLSCompletionPosition &positionInfo,
1437 BackInsertIterator result) const
1438{
1439 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1440
1441 const QQmlJS::SourceLocation colonToken = regions[ColonTokenRegion];
1442
1443 if (afterLocation(colonToken, positionInfo)) {
1444 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1445 return ;
1446 }
1447}
1448
1449void QQmlLSCompletion::insideBinaryExpressionCompletion(
1450 const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo,
1451 BackInsertIterator result) const
1452{
1453 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1454
1455 const QQmlJS::SourceLocation operatorLocation = regions[OperatorTokenRegion];
1456
1457 if (beforeLocation(positionInfo, operatorLocation)) {
1458 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1459 return;
1460 }
1461 if (afterLocation(operatorLocation, positionInfo)) {
1462 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1463 return;
1464 }
1465}
1466
1467/*!
1468\internal
1469Doing completion in variable declarations requires taking a look at all different cases:
1470
1471\list
1472 \li Normal variable names, like \c{let helloWorld = 123;}
1473 In this case, only autocomplete scriptexpressionidentifiers after the '=' token.
1474 Do not propose existing names for the variable name, because the variable name needs to be
1475 an identifier that is not used anywhere (to avoid shadowing and confusing code),
1476
1477 \li Deconstructed arrays, like \c{let [ helloWorld, ] = [ 123, ];}
1478 In this case, only autocomplete scriptexpressionidentifiers after the '=' token.
1479 Do not propose already existing identifiers inside the left hand side array.
1480
1481 \li Deconstructed arrays with initializers, like \c{let [ helloWorld = someVar, ] = [ 123, ];}
1482 Note: this assigns the value of someVar to helloWorld if the right hand side's first element
1483 is undefined or does not exist.
1484
1485 In this case, only autocomplete scriptexpressionidentifiers after the '=' tokens.
1486 Only propose already existing identifiers inside the left hand side array when behind a '='
1487 token.
1488
1489 \li Deconstructed Objects, like \c{let { helloWorld, } = { helloWorld: 123, };}
1490 In this case, only autocomplete scriptexpressionidentifiers after the '=' token.
1491 Do not propose already existing identifiers inside the left hand side object.
1492
1493 \li Deconstructed Objects with initializers, like \c{let { helloWorld = someVar, } = {};}
1494 Note: this assigns the value of someVar to helloWorld if the right hand side's object does
1495 not have a property called 'helloWorld'.
1496
1497 In this case, only autocomplete scriptexpressionidentifiers after the '=' token.
1498 Only propose already existing identifiers inside the left hand side object when behind a '='
1499 token.
1500
1501 \li Finally, you are allowed to nest and combine all above possibilities together for all your
1502 deconstruction needs, so the exact same completion needs to be done for
1503 DomType::ScriptPatternElement too.
1504
1505\endlist
1506*/
1507void QQmlLSCompletion::insideScriptPattern(const DomItem &parentForContext,
1508 const QQmlLSCompletionPosition &positionInfo,
1509 BackInsertIterator result) const
1510{
1511 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1512
1513 const QQmlJS::SourceLocation equal = regions[EqualTokenRegion];
1514
1515 if (!afterLocation(equal, positionInfo))
1516 return;
1517
1518 // otherwise, only complete case and default
1519 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1520}
1521
1522/*!
1523\internal
1524See comment on insideScriptPattern().
1525*/
1526void QQmlLSCompletion::insideVariableDeclarationEntry(const DomItem &parentForContext,
1527 const QQmlLSCompletionPosition &positionInfo,
1528 BackInsertIterator result) const
1529{
1530 insideScriptPattern(parentForContext, positionInfo, result);
1531}
1532
1533void QQmlLSCompletion::insideThrowStatement(const DomItem &parentForContext,
1534 const QQmlLSCompletionPosition &positionInfo,
1535 BackInsertIterator result) const
1536{
1537 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1538
1539 const QQmlJS::SourceLocation throwKeyword = regions[ThrowKeywordRegion];
1540
1541 if (afterLocation(throwKeyword, positionInfo)) {
1542 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1543 return;
1544 }
1545}
1546
1547void QQmlLSCompletion::insideLabelledStatement(const DomItem &parentForContext,
1548 const QQmlLSCompletionPosition &positionInfo,
1549 BackInsertIterator result) const
1550{
1551 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1552
1553 const QQmlJS::SourceLocation colon = regions[ColonTokenRegion];
1554
1555 if (afterLocation(colon, positionInfo)) {
1556 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1557 return;
1558 }
1559 // note: the case "beforeLocation(ctx, colon)" probably never happens:
1560 // this is because without the colon, the parser will probably not parse this as a
1561 // labelledstatement but as a normal expression statement.
1562 // So this case only happens when the colon already exists, and the user goes back to the
1563 // label name and requests completion for that label.
1564}
1565
1566/*!
1567\internal
1568Collect the current set of labels that some DomItem can jump to.
1569*/
1570static void collectLabels(const DomItem &context, QQmlLSCompletion::BackInsertIterator result)
1571{
1572 for (DomItem current = context; current; current = current.directParent()) {
1573 if (current.internalKind() == DomType::ScriptLabelledStatement) {
1574 const QString label = current.field(Fields::label).value().toString();
1575 if (label.isEmpty())
1576 continue;
1577 CompletionItem item;
1578 item.label = label.toUtf8();
1579 item.kind = int(CompletionItemKind::Value); // variable?
1580 // TODO: more stuff here?
1581 result = item;
1582 } else if (current.internalKind() == DomType::ScriptExpression) {
1583 // quick exit when leaving the JS part
1584 return;
1585 }
1586 }
1587 return;
1588}
1589
1590void QQmlLSCompletion::insideContinueStatement(const DomItem &parentForContext,
1591 const QQmlLSCompletionPosition &positionInfo,
1592 BackInsertIterator result) const
1593{
1594 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1595
1596 const QQmlJS::SourceLocation continueKeyword = regions[ContinueKeywordRegion];
1597
1598 if (afterLocation(continueKeyword, positionInfo)) {
1599 collectLabels(parentForContext, result);
1600 return;
1601 }
1602}
1603
1604void QQmlLSCompletion::insideBreakStatement(const DomItem &parentForContext,
1605 const QQmlLSCompletionPosition &positionInfo,
1606 BackInsertIterator result) const
1607{
1608 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1609
1610 const QQmlJS::SourceLocation breakKeyword = regions[BreakKeywordRegion];
1611
1612 if (afterLocation(breakKeyword, positionInfo)) {
1613 collectLabels(parentForContext, result);
1614 return;
1615 }
1616}
1617
1618void QQmlLSCompletion::insideConditionalExpression(const DomItem &parentForContext,
1619 const QQmlLSCompletionPosition &positionInfo,
1620 BackInsertIterator result) const
1621{
1622 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1623
1624 const QQmlJS::SourceLocation questionMark = regions[QuestionMarkTokenRegion];
1625 const QQmlJS::SourceLocation colon = regions[ColonTokenRegion];
1626
1627 if (beforeLocation(positionInfo, questionMark)) {
1628 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1629 return;
1630 }
1631 if (betweenLocations(questionMark, positionInfo, colon)) {
1632 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1633 return;
1634 }
1635 if (afterLocation(colon, positionInfo)) {
1636 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1637 return;
1638 }
1639}
1640
1641void QQmlLSCompletion::insideUnaryExpression(const DomItem &parentForContext,
1642 const QQmlLSCompletionPosition &positionInfo,
1643 BackInsertIterator result) const
1644{
1645 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1646
1647 const QQmlJS::SourceLocation operatorToken = regions[OperatorTokenRegion];
1648
1649 if (afterLocation(operatorToken, positionInfo)) {
1650 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1651 return;
1652 }
1653}
1654
1655void QQmlLSCompletion::insidePostExpression(const DomItem &parentForContext,
1656 const QQmlLSCompletionPosition &positionInfo,
1657 BackInsertIterator result) const
1658{
1659 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1660
1661 const QQmlJS::SourceLocation operatorToken = regions[OperatorTokenRegion];
1662
1663 if (beforeLocation(positionInfo, operatorToken)) {
1664 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1665 return;
1666 }
1667}
1668
1669void QQmlLSCompletion::insideParenthesizedExpression(const DomItem &parentForContext,
1670 const QQmlLSCompletionPosition &positionInfo,
1671 BackInsertIterator result) const
1672{
1673 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1674
1675 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1676 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1677
1678 if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) {
1679 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1680 return;
1681 }
1682}
1683
1684void QQmlLSCompletion::insideTemplateLiteral(const DomItem &parentForContext,
1685 const QQmlLSCompletionPosition &positionInfo,
1686 BackInsertIterator result) const
1687{
1688 Q_UNUSED(parentForContext);
1689 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1690}
1691
1692void QQmlLSCompletion::insideNewExpression(const DomItem &parentForContext,
1693 const QQmlLSCompletionPosition &positionInfo,
1694 BackInsertIterator result) const
1695{
1696 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1697 const QQmlJS::SourceLocation newKeyword = regions[NewKeywordRegion];
1698
1699 if (afterLocation(newKeyword, positionInfo)) {
1700 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1701 return;
1702 }
1703}
1704
1705void QQmlLSCompletion::insideNewMemberExpression(const DomItem &parentForContext,
1706 const QQmlLSCompletionPosition &positionInfo,
1707 BackInsertIterator result) const
1708{
1709 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1710 const QQmlJS::SourceLocation newKeyword = regions[NewKeywordRegion];
1711 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1712 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1713
1714 if (betweenLocations(newKeyword, positionInfo, leftParenthesis)
1715 || betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) {
1716 suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
1717 return;
1718 }
1719}
1720
1721void QQmlLSCompletion::signalHandlerCompletion(const QQmlJSScope::ConstPtr &scope,
1722 QDuplicateTracker<QString> *usedNames,
1723 BackInsertIterator result) const
1724{
1725 const auto keyValues = scope->methods().asKeyValueRange();
1726 for (const auto &[name, method] : keyValues) {
1727 if (method.access() != QQmlJSMetaMethod::Public
1728 || method.methodType() != QQmlJSMetaMethodType::Signal) {
1729 continue;
1730 }
1731 if (usedNames && usedNames->hasSeen(name)) {
1732 continue;
1733 }
1734
1735 CompletionItem completion;
1736 completion.label = QQmlSignalNames::signalNameToHandlerName(name).toUtf8();
1737 completion.kind = int(CompletionItemKind::Method);
1738 result = completion;
1739 }
1740}
1741
1742/*!
1743\internal
1744Decide which completions can be used at currentItem and compute them.
1745*/
1746QList<CompletionItem>
1747QQmlLSCompletion::completions(const DomItem &currentItem,
1748 const CompletionContextStrings &contextStrings) const
1749{
1750 QList<CompletionItem> result;
1751 collectCompletions(currentItem, contextStrings, std::back_inserter(result));
1752 return result;
1753}
1754
1755void QQmlLSCompletion::collectCompletions(const DomItem &currentItem,
1756 const CompletionContextStrings &contextStrings,
1757 BackInsertIterator result) const
1758{
1759 /*!
1760 Completion is not provided on a script identifier expression because script identifier
1761 expressions lack context information. Instead, find the first parent that has enough
1762 context information and provide completion for this one.
1763 For example, a script identifier expression \c{le} in
1764 \badcode
1765 for (;le;) { ... }
1766 \endcode
1767 will get completion for a property called \c{leProperty}, while the same script identifier
1768 expression in
1769 \badcode
1770 for (le;;) { ... }
1771 \endcode
1772 will, in addition to \c{leProperty}, also get completion for the \c{let} statement snippet.
1773 In this example, the parent used for the completion is the for-statement, of type
1774 DomType::ScriptForStatement.
1775
1776 In addition of the parent for the context, use positionInfo to have exact information on where
1777 the cursor is (to compare with the SourceLocations of tokens) and which item is at this position
1778 (required to provide completion at the correct position, for example for attached properties).
1779 */
1780 const QQmlLSCompletionPosition positionInfo{ currentItem, contextStrings };
1781 for (DomItem currentParent = currentItem; currentParent;
1782 currentParent = currentParent.directParent()) {
1783 const DomType currentType = currentParent.internalKind();
1784
1785 switch (currentType) {
1786 case DomType::Id:
1787 // suppress completions for ids
1788 return;
1789 case DomType::Pragma:
1790 insidePragmaCompletion(currentParent, positionInfo, result);
1791 return;
1792 case DomType::ScriptType: {
1793 if (currentParent.directParent().internalKind() == DomType::QmlObject) {
1794 insideQmlObjectCompletion(currentParent.directParent(), positionInfo, result);
1795 return;
1796 }
1797
1798 LocalSymbolsTypes options;
1799 options.setFlag(LocalSymbolsType::ObjectType);
1800 options.setFlag(LocalSymbolsType::ValueType);
1801 suggestReachableTypes(currentItem, options, CompletionItemKind::Class, result);
1802 return;
1803 }
1804 case DomType::ScriptFormalParameter:
1805 // no autocompletion inside of function parameter definition
1806 return;
1807 case DomType::Binding:
1808 insideBindingCompletion(currentParent, positionInfo, result);
1809 return;
1810 case DomType::Import:
1811 insideImportCompletion(currentParent, positionInfo, result);
1812 return;
1813 case DomType::ScriptForStatement:
1814 insideForStatementCompletion(currentParent, positionInfo, result);
1815 return;
1816 case DomType::ScriptBlockStatement:
1817 suggestJSStatementCompletion(positionInfo.itemAtPosition, result);
1818 return;
1819 case DomType::QmlFile:
1820 insideQmlFileCompletion(currentParent, positionInfo, result);
1821 return;
1822 case DomType::QmlObject:
1823 insideQmlObjectCompletion(currentParent, positionInfo, result);
1824 return;
1825 case DomType::MethodInfo:
1826 // suppress completions
1827 return;
1828 case DomType::PropertyDefinition:
1829 insidePropertyDefinitionCompletion(currentParent, positionInfo, result);
1830 return;
1831 case DomType::ScriptBinaryExpression:
1832 // ignore field member expressions: these need additional context from its parents
1833 if (QQmlLSUtils::isFieldMemberExpression(currentParent))
1834 continue;
1835 insideBinaryExpressionCompletion(currentParent, positionInfo, result);
1836 return;
1837 case DomType::ScriptLiteral:
1838 insideScriptLiteralCompletion(currentParent, positionInfo, result);
1839 return;
1840 case DomType::ScriptRegExpLiteral:
1841 // no completion inside of regexp literals
1842 return;
1843 case DomType::ScriptCallExpression:
1844 insideCallExpression(currentParent, positionInfo, result);
1845 return;
1846 case DomType::ScriptIfStatement:
1847 insideIfStatement(currentParent, positionInfo, result);
1848 return;
1849 case DomType::ScriptReturnStatement:
1850 insideReturnStatement(currentParent, positionInfo, result);
1851 return;
1852 case DomType::ScriptWhileStatement:
1853 insideWhileStatement(currentParent, positionInfo, result);
1854 return;
1855 case DomType::ScriptDoWhileStatement:
1856 insideDoWhileStatement(currentParent, positionInfo, result);
1857 return;
1858 case DomType::ScriptForEachStatement:
1859 insideForEachStatement(currentParent, positionInfo, result);
1860 return;
1861 case DomType::ScriptTryCatchStatement:
1862 /*!
1863 \internal
1864 The Ecmascript standard specifies that there can only be a block statement between \c
1865 try and \c catch(...), \c try and \c finally and \c catch(...) and \c finally, so all of
1866 these completions are already handled by the DomType::ScriptBlockStatement completion.
1867 The only place in the try statement where there is no BlockStatement and therefore needs
1868 its own completion is inside the catch parameter, but that is
1869 \quotation
1870 An optional identifier or pattern to hold the caught exception for the associated catch
1871 block.
1872 \endquotation
1873 citing
1874 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch?retiredLocale=de#exceptionvar.
1875 This means that no completion is needed inside a catch-expression, as it should contain
1876 an identifier that is not yet used anywhere.
1877 Therefore, no completion is required at all when inside a try-statement but outside a
1878 block-statement.
1879 */
1880 return;
1881 case DomType::ScriptSwitchStatement:
1882 insideSwitchStatement(currentParent, positionInfo, result);
1883 return;
1884 case DomType::ScriptCaseClause:
1885 insideCaseClause(currentParent, positionInfo, result);
1886 return;
1887 case DomType::ScriptDefaultClause:
1888 if (ctxBeforeStatement(positionInfo, currentParent, QQmlJS::Dom::DefaultKeywordRegion))
1889 continue;
1890 insideDefaultClause(currentParent, positionInfo, result);
1891 return;
1892 case DomType::ScriptCaseBlock:
1893 insideCaseBlock(currentParent, positionInfo, result);
1894 return;
1895 case DomType::ScriptVariableDeclaration:
1896 // not needed: thats a list of ScriptVariableDeclarationEntry, and those entries cannot
1897 // be suggested because they all start with `{`, `[` or an identifier that should not be
1898 // in use yet.
1899 return;
1900 case DomType::ScriptVariableDeclarationEntry:
1901 insideVariableDeclarationEntry(currentParent, positionInfo, result);
1902 return;
1903 case DomType::ScriptProperty:
1904 // fallthrough: a ScriptProperty is a ScriptPattern but inside a JS Object. It gets the
1905 // same completions as a ScriptPattern.
1906 case DomType::ScriptPattern:
1907 insideScriptPattern(currentParent, positionInfo, result);
1908 return;
1909 case DomType::ScriptThrowStatement:
1910 insideThrowStatement(currentParent, positionInfo, result);
1911 return;
1912 case DomType::ScriptLabelledStatement:
1913 insideLabelledStatement(currentParent, positionInfo, result);
1914 return;
1915 case DomType::ScriptContinueStatement:
1916 insideContinueStatement(currentParent, positionInfo, result);
1917 return;
1918 case DomType::ScriptBreakStatement:
1919 insideBreakStatement(currentParent, positionInfo, result);
1920 return;
1921 case DomType::ScriptConditionalExpression:
1922 insideConditionalExpression(currentParent, positionInfo, result);
1923 return;
1924 case DomType::ScriptUnaryExpression:
1925 insideUnaryExpression(currentParent, positionInfo, result);
1926 return;
1927 case DomType::ScriptPostExpression:
1928 insidePostExpression(currentParent, positionInfo, result);
1929 return;
1930 case DomType::ScriptParenthesizedExpression:
1931 insideParenthesizedExpression(currentParent, positionInfo, result);
1932 return;
1933 case DomType::ScriptTemplateLiteral:
1934 insideTemplateLiteral(currentParent, positionInfo, result);
1935 return;
1936 case DomType::ScriptTemplateStringPart:
1937 // no completion inside of the non-expression parts of template strings
1938 return;
1939 case DomType::ScriptNewExpression:
1940 insideNewExpression(currentParent, positionInfo, result);
1941 return;
1942 case DomType::ScriptNewMemberExpression:
1943 insideNewMemberExpression(currentParent, positionInfo, result);
1944 return;
1945 case DomType::ScriptThisExpression:
1946 // suppress completions on `this`
1947 return;
1948 case DomType::ScriptSuperLiteral:
1949 // suppress completions on `super`
1950 return;
1951 case DomType::Comment:
1952 // no completion inside of comments
1953 return;
1954
1955 // TODO: Implement those statements.
1956 // In the meanwhile, suppress completions to avoid weird behaviors.
1957 case DomType::ScriptArray:
1958 case DomType::ScriptObject:
1959 case DomType::ScriptElision:
1960 case DomType::ScriptArrayEntry:
1961 return;
1962
1963 default:
1964 continue;
1965 }
1966 Q_UNREACHABLE();
1967 }
1968
1969 // no completion could be found
1970 qCDebug(QQmlLSUtilsLog) << "No completion was found for current request.";
1971 return;
1972}
1973
1974QQmlLSCompletion::QQmlLSCompletion(const QFactoryLoader &pluginLoader)
1975{
1976 const auto keys = pluginLoader.metaDataKeys();
1977 for (qsizetype i = 0; i < keys.size(); ++i) {
1978 auto instance = std::unique_ptr<QQmlLSPlugin>(
1979 qobject_cast<QQmlLSPlugin *>(pluginLoader.instance(i)));
1980 if (!instance)
1981 continue;
1982 if (auto completionInstance = instance->createCompletionPlugin())
1983 m_plugins.push_back(std::move(completionInstance));
1984 }
1985}
1986
1987/*!
1988\internal
1989Helper method to call a method on all loaded plugins.
1990*/
1991void QQmlLSCompletion::collectFromPlugins(qxp::function_ref<CompletionFromPluginFunction> f,
1992 BackInsertIterator result) const
1993{
1994 for (const auto &plugin : m_plugins) {
1995 Q_ASSERT(plugin);
1996 f(plugin.get(), result);
1997 }
1998}
1999
2000void QQmlLSCompletion::suggestSnippetsForLeftHandSideOfBinding(const DomItem &itemAtPosition,
2001 BackInsertIterator result) const
2002{
2003 collectFromPlugins(
2004 [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) {
2005 p->suggestSnippetsForLeftHandSideOfBinding(itemAtPosition, result);
2006 },
2007 result);
2008}
2009
2010void QQmlLSCompletion::suggestSnippetsForRightHandSideOfBinding(const DomItem &itemAtPosition,
2011 BackInsertIterator result) const
2012{
2013 collectFromPlugins(
2014 [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) {
2015 p->suggestSnippetsForRightHandSideOfBinding(itemAtPosition, result);
2016 },
2017 result);
2018}
2019
2020QT_END_NAMESPACE
void collectFromAllJavaScriptParents(const F &&f, const QQmlJSScope::ConstPtr &scope)
static const QQmlJSScope * resolve(const QQmlJSScope *current, const QStringList &names)
static void collectLabels(const DomItem &context, QQmlLSCompletion::BackInsertIterator result)
static bool testScopeSymbol(const QQmlJSScope::ConstPtr &scope, LocalSymbolsTypes options, CompletionItemKind kind)