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

source code of qtdeclarative/src/qmlls/qqmllscompletion.cpp