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
6using namespace QLspSpecification;
7using namespace QQmlJS::Dom;
8using namespace Qt::StringLiterals;
9
10QT_BEGIN_NAMESPACE
11
12Q_LOGGING_CATEGORY(QQmlLSCompletionLog, "qt.languageserver.completions")
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.offset;
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().offset < 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/required property completion
764 for (QUtf8StringView view :
765 std::array<QUtf8StringView, 6>{ "", "readonly ", "default ", "default required ",
766 "required default ", "required " }) {
767 // readonly properties require an initializer
768 if (view != QUtf8StringView("readonly ")) {
769 result = makeSnippet(
770 label: QByteArray(view.data()).append(s: "property type name;"),
771 insertText: QByteArray(view.data()).append(s: "property ${1:type} ${0:name};"));
772 }
773
774 result = makeSnippet(
775 label: QByteArray(view.data()).append(s: "property type name: value;"),
776 insertText: QByteArray(view.data()).append(s: "property ${1:type} ${2:name}: ${0:value};"));
777 }
778
779 // signal
780 result = makeSnippet(label: "signal name(arg1:type1, ...)", insertText: "signal ${1:name}($0)");
781
782 // signal without parameters
783 result = makeSnippet(label: "signal name;", insertText: "signal ${0:name};");
784
785 // make already existing property required
786 result = makeSnippet(label: "required name;", insertText: "required ${0:name};");
787
788 // function
789 result = makeSnippet(label: "function name(args...): returnType { statements...}",
790 insertText: "function ${1:name}($2): ${3:returnType} {\n\t$0\n}");
791
792 // enum
793 result = makeSnippet(label: "enum name { Values...}", insertText: "enum ${1:name} {\n\t${0:values}\n}");
794
795 // inline component
796 result = makeSnippet(label: "component Name: BaseType { ... }",
797 insertText: "component ${1:name}: ${2:baseType} {\n\t$0\n}");
798
799 suggestBindingCompletion(itemAtPosition: positionInfo.itemAtPosition, it: result);
800
801 // add Qml Types for default binding
802 const DomItem containingFile = parentForContext.containingFile();
803 suggestReachableTypes(el: containingFile, options: LocalSymbolsType::ObjectType,
804 kind: CompletionItemKind::Constructor, it: result);
805 suggestSnippetsForLeftHandSideOfBinding(items: positionInfo.itemAtPosition, result);
806 return;
807 }
808}
809
810void QQmlLSCompletion::insidePropertyDefinitionCompletion(
811 const DomItem &currentItem, const QQmlLSCompletionPosition &positionInfo,
812 BackInsertIterator result) const
813{
814 auto info = FileLocations::treeOf(currentItem)->info();
815 const QQmlJS::SourceLocation propertyKeyword = info.regions[PropertyKeywordRegion];
816
817 // do completions for the keywords
818 if (positionInfo.offset() < propertyKeyword.offset + propertyKeyword.length) {
819 const QQmlJS::SourceLocation readonlyKeyword = info.regions[ReadonlyKeywordRegion];
820 const QQmlJS::SourceLocation defaultKeyword = info.regions[DefaultKeywordRegion];
821 const QQmlJS::SourceLocation requiredKeyword = info.regions[RequiredKeywordRegion];
822
823 bool completeReadonly = true;
824 bool completeRequired = true;
825 bool completeDefault = true;
826
827 // if there is already a readonly keyword before the cursor: do not auto complete it again
828 if (readonlyKeyword.isValid() && readonlyKeyword.offset < positionInfo.offset()) {
829 completeReadonly = false;
830 // also, required keywords do not like readonly keywords
831 completeRequired = false;
832 }
833
834 // same for required
835 if (requiredKeyword.isValid() && requiredKeyword.offset < positionInfo.offset()) {
836 completeRequired = false;
837 // also, required keywords do not like readonly keywords
838 completeReadonly = false;
839 }
840
841 // same for default
842 if (defaultKeyword.isValid() && defaultKeyword.offset < positionInfo.offset()) {
843 completeDefault = false;
844 }
845 auto addCompletionKeyword = [&result](QUtf8StringView view, bool complete) {
846 if (!complete)
847 return;
848 CompletionItem item;
849 item.label = view.data();
850 item.kind = int(CompletionItemKind::Keyword);
851 result = item;
852 };
853 addCompletionKeyword(u8"readonly", completeReadonly);
854 addCompletionKeyword(u8"required", completeRequired);
855 addCompletionKeyword(u8"default", completeDefault);
856 addCompletionKeyword(u8"property", true);
857
858 return;
859 }
860
861 const QQmlJS::SourceLocation propertyIdentifier = info.regions[IdentifierRegion];
862 if (propertyKeyword.end() <= positionInfo.offset()
863 && positionInfo.offset() < propertyIdentifier.offset) {
864 suggestReachableTypes(el: currentItem,
865 options: LocalSymbolsType::ObjectType | LocalSymbolsType::ValueType,
866 kind: CompletionItemKind::Class, it: result);
867 }
868 // do not autocomplete the rest
869 return;
870}
871
872void QQmlLSCompletion::insideBindingCompletion(const DomItem &currentItem,
873 const QQmlLSCompletionPosition &positionInfo,
874 BackInsertIterator result) const
875{
876 const DomItem containingBinding = currentItem.filterUp(
877 filter: [](DomType type, const QQmlJS::Dom::DomItem &) { return type == DomType::Binding; },
878 options: FilterUpOptions::ReturnOuter);
879
880 // do scriptidentifiercompletion after the ':' of a binding
881 if (cursorAfterColon(currentItem: containingBinding, positionInfo)) {
882 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
883
884 if (auto type = QQmlLSUtils::resolveExpressionType(item: currentItem,
885 QQmlLSUtils::ResolveOwnerType)) {
886 const QStringList names = currentItem.field(name: Fields::name).toString().split(sep: u'.');
887 const QQmlJSScope *current = resolve(current: type->semanticScope.get(), names);
888 // add type names when binding to an object type or a property with var type
889 if (!current || current->accessSemantics() == QQmlSA::AccessSemantics::Reference) {
890 LocalSymbolsTypes options;
891 options.setFlag(flag: LocalSymbolsType::ObjectType);
892 suggestReachableTypes(el: positionInfo.itemAtPosition, options,
893 kind: CompletionItemKind::Constructor, it: result);
894 suggestSnippetsForRightHandSideOfBinding(items: positionInfo.itemAtPosition, result);
895 }
896 }
897 return;
898 }
899
900 // ignore the binding if asking for completion in front of the binding
901 if (cursorInFrontOfItem(parentForContext: containingBinding, positionInfo)) {
902 insideQmlObjectCompletion(parentForContext: currentItem.containingObject(), positionInfo, result);
903 return;
904 }
905
906 const DomItem containingObject = currentItem.qmlObject();
907
908 suggestBindingCompletion(itemAtPosition: positionInfo.itemAtPosition, it: result);
909
910 // add Qml Types for default binding
911 suggestReachableTypes(el: positionInfo.itemAtPosition, options: LocalSymbolsType::ObjectType,
912 kind: CompletionItemKind::Constructor, it: result);
913 suggestSnippetsForLeftHandSideOfBinding(items: positionInfo.itemAtPosition, result);
914}
915
916void QQmlLSCompletion::insideImportCompletion(const DomItem &currentItem,
917 const QQmlLSCompletionPosition &positionInfo,
918 BackInsertIterator result) const
919{
920 const DomItem containingFile = currentItem.containingFile();
921 insideImportCompletionHelper(file: containingFile, positionInfo, it: result);
922
923 // when in front of the import statement: propose types for root Qml Object completion
924 if (cursorInFrontOfItem(parentForContext: currentItem, positionInfo)) {
925 suggestReachableTypes(el: containingFile, options: LocalSymbolsType::ObjectType,
926 kind: CompletionItemKind::Constructor, it: result);
927 }
928}
929
930void QQmlLSCompletion::insideQmlFileCompletion(const DomItem &currentItem,
931 const QQmlLSCompletionPosition &positionInfo,
932 BackInsertIterator result) const
933{
934 const DomItem containingFile = currentItem.containingFile();
935 // completions for code outside the root Qml Object
936 // global completions
937 if (positionInfo.cursorPosition.atLineStart()) {
938 if (positionInfo.cursorPosition.base().isEmpty()) {
939 for (const QStringView &s : std::array<QStringView, 2>({ u"pragma", u"import" })) {
940 CompletionItem comp;
941 comp.label = s.toUtf8();
942 comp.kind = int(CompletionItemKind::Keyword);
943 result = comp;
944 }
945 }
946 }
947 // Types for root Qml Object completion
948 suggestReachableTypes(el: containingFile, options: LocalSymbolsType::ObjectType,
949 kind: CompletionItemKind::Constructor, it: result);
950}
951
952/*!
953\internal
954Generate the snippets for let, var and const variable declarations.
955*/
956void QQmlLSCompletion::suggestVariableDeclarationStatementCompletion(
957 BackInsertIterator result, AppendOption option) const
958{
959 // let/var/const statement
960 for (auto view : std::array<QUtf8StringView, 3>{ "let", "var", "const" }) {
961 auto snippet = makeSnippet(label: QByteArray(view.data()).append(s: " variable = value"),
962 insertText: QByteArray(view.data()).append(s: " ${1:variable} = $0"));
963 if (option == AppendSemicolon) {
964 snippet.insertText->append(s: ";");
965 snippet.label.append(s: ";");
966 }
967 result = snippet;
968 }
969}
970
971/*!
972\internal
973Generate the snippets for case and default statements.
974*/
975void QQmlLSCompletion::suggestCaseAndDefaultStatementCompletion(BackInsertIterator result) const
976{
977 // case snippet
978 result = makeSnippet(label: "case value: statements...", insertText: "case ${1:value}:\n\t$0");
979 // case + brackets snippet
980 result = makeSnippet(label: "case value: { statements... }", insertText: "case ${1:value}: {\n\t$0\n}");
981
982 // default snippet
983 result = makeSnippet(label: "default: statements...", insertText: "default:\n\t$0");
984 // default + brackets snippet
985 result = makeSnippet(label: "default: { statements... }", insertText: "default: {\n\t$0\n}");
986}
987
988/*!
989\internal
990Break and continue can be inserted only in following situations:
991\list
992 \li Break and continue inside a loop.
993 \li Break inside a (nested) LabelledStatement
994 \li Break inside a (nested) SwitchStatement
995\endlist
996*/
997void QQmlLSCompletion::suggestContinueAndBreakStatementIfNeeded(const DomItem &itemAtPosition,
998 BackInsertIterator result) const
999{
1000 bool alreadyInLabel = false;
1001 bool alreadyInSwitch = false;
1002 for (DomItem current = itemAtPosition; current; current = current.directParent()) {
1003 switch (current.internalKind()) {
1004 case DomType::ScriptExpression:
1005 // reached end of script expression
1006 return;
1007
1008 case DomType::ScriptForStatement:
1009 case DomType::ScriptForEachStatement:
1010 case DomType::ScriptWhileStatement:
1011 case DomType::ScriptDoWhileStatement: {
1012 CompletionItem continueKeyword;
1013 continueKeyword.label = "continue";
1014 continueKeyword.kind = int(CompletionItemKind::Keyword);
1015 result = continueKeyword;
1016
1017 // do not add break twice
1018 if (!alreadyInSwitch && !alreadyInLabel) {
1019 CompletionItem breakKeyword;
1020 breakKeyword.label = "break";
1021 breakKeyword.kind = int(CompletionItemKind::Keyword);
1022 result = breakKeyword;
1023 }
1024 // early exit: cannot suggest more completions
1025 return;
1026 }
1027 case DomType::ScriptSwitchStatement: {
1028 // check if break was already inserted
1029 if (alreadyInSwitch || alreadyInLabel)
1030 break;
1031 alreadyInSwitch = true;
1032
1033 CompletionItem breakKeyword;
1034 breakKeyword.label = "break";
1035 breakKeyword.kind = int(CompletionItemKind::Keyword);
1036 result = breakKeyword;
1037 break;
1038 }
1039 case DomType::ScriptLabelledStatement: {
1040 // check if break was already inserted because of switch or loop
1041 if (alreadyInSwitch || alreadyInLabel)
1042 break;
1043 alreadyInLabel = true;
1044
1045 CompletionItem breakKeyword;
1046 breakKeyword.label = "break";
1047 breakKeyword.kind = int(CompletionItemKind::Keyword);
1048 result = breakKeyword;
1049 break;
1050 }
1051 default:
1052 break;
1053 }
1054 }
1055}
1056
1057/*!
1058\internal
1059Generates snippets or keywords for all possible JS statements where it makes sense. To use whenever
1060any JS statement can be expected, but when no JS statement is there yet.
1061
1062Only generates JS expression completions when itemAtPosition is a qualified name.
1063
1064Here is a list of statements that do \e{not} get any snippets:
1065\list
1066 \li BlockStatement does not need a code snippet, editors automatically include the closing
1067bracket anyway. \li EmptyStatement completion would only generate a single \c{;} \li
1068ExpressionStatement completion cannot generate any snippet, only identifiers \li WithStatement
1069completion is not recommended: qmllint will warn about usage of with statements \li
1070LabelledStatement completion might need to propose labels (TODO?) \li DebuggerStatement completion
1071does not strike as being very useful \endlist
1072*/
1073void QQmlLSCompletion::suggestJSStatementCompletion(const DomItem &itemAtPosition,
1074 BackInsertIterator result) const
1075{
1076 suggestJSExpressionCompletion(scriptIdentifier: itemAtPosition, result);
1077
1078 if (QQmlLSUtils::isFieldMemberAccess(item: itemAtPosition)
1079 || QQmlLSUtils::isFieldMemberExpression(item: itemAtPosition))
1080 return;
1081
1082 // expression statements
1083 suggestVariableDeclarationStatementCompletion(result);
1084 // block statement
1085 result = makeSnippet(label: "{ statements... }", insertText: "{\n\t$0\n}");
1086
1087 // if + brackets statement
1088 result = makeSnippet(label: "if (condition) { statements }", insertText: "if ($1) {\n\t$0\n}");
1089
1090 // do statement
1091 result = makeSnippet(label: "do { statements } while (condition);", insertText: "do {\n\t$1\n} while ($0);");
1092
1093 // while + brackets statement
1094 result = makeSnippet(label: "while (condition) { statements...}", insertText: "while ($1) {\n\t$0\n}");
1095
1096 // for + brackets loop statement
1097 result = makeSnippet(label: "for (initializer; condition; increment) { statements... }",
1098 insertText: "for ($1;$2;$3) {\n\t$0\n}");
1099
1100 // for ... in + brackets loop statement
1101 result = makeSnippet(label: "for (property in object) { statements... }", insertText: "for ($1 in $2) {\n\t$0\n}");
1102
1103 // for ... of + brackets loop statement
1104 result = makeSnippet(label: "for (element of array) { statements... }", insertText: "for ($1 of $2) {\n\t$0\n}");
1105
1106 // try + catch statement
1107 result = makeSnippet(label: "try { statements... } catch(error) { statements... }",
1108 insertText: "try {\n\t$1\n} catch($2) {\n\t$0\n}");
1109
1110 // try + finally statement
1111 result = makeSnippet(label: "try { statements... } finally { statements... }",
1112 insertText: "try {\n\t$1\n} finally {\n\t$0\n}");
1113
1114 // try + catch + finally statement
1115 result = makeSnippet(
1116 label: "try { statements... } catch(error) { statements... } finally { statements... }",
1117 insertText: "try {\n\t$1\n} catch($2) {\n\t$3\n} finally {\n\t$0\n}");
1118
1119 // one can always assume that JS code in QML is inside a function, so always propose `return`
1120 for (auto &&view : { "return"_ba, "throw"_ba }) {
1121 CompletionItem item;
1122 item.label = std::move(view);
1123 item.kind = int(CompletionItemKind::Keyword);
1124 result = item;
1125 }
1126
1127 // rules for case+default statements:
1128 // 1) when inside a CaseBlock, or
1129 // 2) inside a CaseClause, as an (non-nested) element of the CaseClause statementlist.
1130 // 3) inside a DefaultClause, as an (non-nested) element of the DefaultClause statementlist,
1131 //
1132 // switch (x) {
1133 // // (1)
1134 // case 1:
1135 // myProperty = 5;
1136 // // (2) -> could be another statement of current case, but also a new case or default!
1137 // default:
1138 // myProperty = 5;
1139 // // (3) -> could be another statement of current default, but also a new case or default!
1140 // }
1141 const DomType currentKind = itemAtPosition.internalKind();
1142 const DomType parentKind = itemAtPosition.directParent().internalKind();
1143 if (currentKind == DomType::ScriptCaseBlock || currentKind == DomType::ScriptCaseClause
1144 || currentKind == DomType::ScriptDefaultClause
1145 || (currentKind == DomType::List
1146 && (parentKind == DomType::ScriptCaseClause
1147 || parentKind == DomType::ScriptDefaultClause))) {
1148 suggestCaseAndDefaultStatementCompletion(result);
1149 }
1150 suggestContinueAndBreakStatementIfNeeded(itemAtPosition, result);
1151}
1152
1153void QQmlLSCompletion::insideForStatementCompletion(const DomItem &parentForContext,
1154 const QQmlLSCompletionPosition &positionInfo,
1155 BackInsertIterator result) const
1156{
1157 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1158
1159 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1160 const QQmlJS::SourceLocation firstSemicolon = regions[FirstSemicolonTokenRegion];
1161 const QQmlJS::SourceLocation secondSemicolon = regions[SecondSemicolonRegion];
1162 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1163
1164 if (betweenLocations(left: leftParenthesis, positionInfo, right: firstSemicolon)) {
1165 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1166 suggestVariableDeclarationStatementCompletion(result,
1167 option: AppendOption::AppendNothing);
1168 return;
1169 }
1170 if (betweenLocations(left: firstSemicolon, positionInfo, right: secondSemicolon)
1171 || betweenLocations(left: secondSemicolon, positionInfo, right: rightParenthesis)) {
1172 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1173 return;
1174 }
1175
1176 if (afterLocation(left: rightParenthesis, positionInfo)) {
1177 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1178 return;
1179 }
1180}
1181
1182void QQmlLSCompletion::insideScriptLiteralCompletion(const DomItem &currentItem,
1183 const QQmlLSCompletionPosition &positionInfo,
1184 BackInsertIterator result) const
1185{
1186 Q_UNUSED(currentItem);
1187 if (positionInfo.cursorPosition.base().isEmpty()) {
1188 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1189 return;
1190 }
1191}
1192
1193void QQmlLSCompletion::insideCallExpression(const DomItem &currentItem,
1194 const QQmlLSCompletionPosition &positionInfo,
1195 BackInsertIterator result) const
1196{
1197 const auto regions = FileLocations::treeOf(currentItem)->info().regions;
1198 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1199 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1200 if (beforeLocation(ctx: positionInfo, right: leftParenthesis)) {
1201 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1202 return;
1203 }
1204 if (betweenLocations(left: leftParenthesis, positionInfo, right: rightParenthesis)) {
1205 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1206 return;
1207 }
1208}
1209
1210void QQmlLSCompletion::insideIfStatement(const DomItem &currentItem,
1211 const QQmlLSCompletionPosition &positionInfo,
1212 BackInsertIterator result) const
1213{
1214 const auto regions = FileLocations::treeOf(currentItem)->info().regions;
1215 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1216 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1217 const QQmlJS::SourceLocation elseKeyword = regions[ElseKeywordRegion];
1218
1219 if (betweenLocations(left: leftParenthesis, positionInfo, right: rightParenthesis)) {
1220 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1221 return;
1222 }
1223 if (betweenLocations(left: rightParenthesis, positionInfo, right: elseKeyword)) {
1224 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1225 return;
1226 }
1227 if (afterLocation(left: elseKeyword, positionInfo)) {
1228 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1229 return;
1230 }
1231}
1232
1233void QQmlLSCompletion::insideReturnStatement(const DomItem &currentItem,
1234 const QQmlLSCompletionPosition &positionInfo,
1235 BackInsertIterator result) const
1236{
1237 const auto regions = FileLocations::treeOf(currentItem)->info().regions;
1238 const QQmlJS::SourceLocation returnKeyword = regions[ReturnKeywordRegion];
1239
1240 if (afterLocation(left: returnKeyword, positionInfo)) {
1241 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1242 return;
1243 }
1244}
1245
1246void QQmlLSCompletion::insideWhileStatement(const DomItem &currentItem,
1247 const QQmlLSCompletionPosition &positionInfo,
1248 BackInsertIterator result) const
1249{
1250 const auto regions = FileLocations::treeOf(currentItem)->info().regions;
1251 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1252 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1253
1254 if (betweenLocations(left: leftParenthesis, positionInfo, right: rightParenthesis)) {
1255 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1256 return;
1257 }
1258 if (afterLocation(left: rightParenthesis, positionInfo)) {
1259 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1260 return;
1261 }
1262}
1263
1264void QQmlLSCompletion::insideDoWhileStatement(const DomItem &parentForContext,
1265 const QQmlLSCompletionPosition &positionInfo,
1266 BackInsertIterator result) const
1267{
1268 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1269 const QQmlJS::SourceLocation doKeyword = regions[DoKeywordRegion];
1270 const QQmlJS::SourceLocation whileKeyword = regions[WhileKeywordRegion];
1271 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1272 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1273
1274 if (betweenLocations(left: doKeyword, positionInfo, right: whileKeyword)) {
1275 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1276 return;
1277 }
1278 if (betweenLocations(left: leftParenthesis, positionInfo, right: rightParenthesis)) {
1279 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1280 return;
1281 }
1282}
1283
1284void QQmlLSCompletion::insideForEachStatement(const DomItem &parentForContext,
1285 const QQmlLSCompletionPosition &positionInfo,
1286 BackInsertIterator result) const
1287{
1288 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1289
1290 const QQmlJS::SourceLocation inOf = regions[InOfTokenRegion];
1291 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1292 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1293
1294 if (betweenLocations(left: leftParenthesis, positionInfo, right: inOf)) {
1295 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1296 suggestVariableDeclarationStatementCompletion(result);
1297 return;
1298 }
1299 if (betweenLocations(left: inOf, positionInfo, right: rightParenthesis)) {
1300 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1301 return;
1302 }
1303
1304 if (afterLocation(left: rightParenthesis, positionInfo)) {
1305 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1306 return;
1307 }
1308}
1309
1310void QQmlLSCompletion::insideSwitchStatement(const DomItem &parentForContext,
1311 const QQmlLSCompletionPosition positionInfo,
1312 BackInsertIterator result) const
1313{
1314 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1315
1316 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1317 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1318
1319 if (betweenLocations(left: leftParenthesis, positionInfo, right: rightParenthesis)) {
1320 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1321 return;
1322 }
1323}
1324
1325void QQmlLSCompletion::insideCaseClause(const DomItem &parentForContext,
1326 const QQmlLSCompletionPosition &positionInfo,
1327 BackInsertIterator result) const
1328{
1329 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1330
1331 const QQmlJS::SourceLocation caseKeyword = regions[CaseKeywordRegion];
1332 const QQmlJS::SourceLocation colonToken = regions[ColonTokenRegion];
1333
1334 if (betweenLocations(left: caseKeyword, positionInfo, right: colonToken)) {
1335 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1336 return;
1337 }
1338 if (afterLocation(left: colonToken, positionInfo)) {
1339 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1340 return;
1341 }
1342
1343}
1344
1345/*!
1346\internal
1347Checks if a case or default clause does happen before ctx in the code.
1348*/
1349bool QQmlLSCompletion::isCaseOrDefaultBeforeCtx(const DomItem &currentClause,
1350 const QQmlLSCompletionPosition &positionInfo,
1351 FileLocationRegion keywordRegion) const
1352{
1353 Q_ASSERT(keywordRegion == QQmlJS::Dom::CaseKeywordRegion
1354 || keywordRegion == QQmlJS::Dom::DefaultKeywordRegion);
1355
1356 if (!currentClause)
1357 return false;
1358
1359 const auto token = FileLocations::treeOf(currentClause)->info().regions[keywordRegion];
1360 if (afterLocation(left: token, positionInfo))
1361 return true;
1362
1363 return false;
1364}
1365
1366/*!
1367\internal
1368
1369Search for a `case ...:` or a `default: ` clause happening before ctx, and return the
1370corresponding DomItem of type DomType::CaseClauses or DomType::DefaultClause.
1371
1372Return an empty DomItem if neither case nor default was found.
1373*/
1374DomItem
1375QQmlLSCompletion::previousCaseOfCaseBlock(const DomItem &parentForContext,
1376 const QQmlLSCompletionPosition &positionInfo) const
1377{
1378 const DomItem caseClauses = parentForContext.field(name: Fields::caseClauses);
1379 for (int i = 0; i < caseClauses.indexes(); ++i) {
1380 const DomItem currentClause = caseClauses.index(i);
1381 if (isCaseOrDefaultBeforeCtx(currentClause, positionInfo, keywordRegion: QQmlJS::Dom::CaseKeywordRegion)) {
1382 return currentClause;
1383 }
1384 }
1385
1386 const DomItem defaultClause = parentForContext.field(name: Fields::defaultClause);
1387 if (isCaseOrDefaultBeforeCtx(currentClause: defaultClause, positionInfo, keywordRegion: QQmlJS::Dom::DefaultKeywordRegion))
1388 return parentForContext.field(name: Fields::defaultClause);
1389
1390 const DomItem moreCaseClauses = parentForContext.field(name: Fields::moreCaseClauses);
1391 for (int i = 0; i < moreCaseClauses.indexes(); ++i) {
1392 const DomItem currentClause = moreCaseClauses.index(i);
1393 if (isCaseOrDefaultBeforeCtx(currentClause, positionInfo, keywordRegion: QQmlJS::Dom::CaseKeywordRegion)) {
1394 return currentClause;
1395 }
1396 }
1397
1398 return {};
1399}
1400
1401void QQmlLSCompletion::insideCaseBlock(const DomItem &parentForContext,
1402 const QQmlLSCompletionPosition &positionInfo,
1403 BackInsertIterator result) const
1404{
1405 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1406
1407 const QQmlJS::SourceLocation leftBrace = regions[LeftBraceRegion];
1408 const QQmlJS::SourceLocation rightBrace = regions[RightBraceRegion];
1409
1410 if (!betweenLocations(left: leftBrace, positionInfo, right: rightBrace))
1411 return;
1412
1413 // TODO: looks fishy
1414 // if there is a previous case or default clause, you can still add statements to it
1415 if (const auto previousCase = previousCaseOfCaseBlock(parentForContext, positionInfo)) {
1416 suggestJSStatementCompletion(itemAtPosition: previousCase, result);
1417 return;
1418 }
1419
1420 // otherwise, only complete case and default
1421 suggestCaseAndDefaultStatementCompletion(result);
1422}
1423
1424void QQmlLSCompletion::insideDefaultClause(const DomItem &parentForContext,
1425 const QQmlLSCompletionPosition &positionInfo,
1426 BackInsertIterator result) const
1427{
1428 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1429
1430 const QQmlJS::SourceLocation colonToken = regions[ColonTokenRegion];
1431
1432 if (afterLocation(left: colonToken, positionInfo)) {
1433 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1434 return ;
1435 }
1436}
1437
1438void QQmlLSCompletion::insideBinaryExpressionCompletion(
1439 const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo,
1440 BackInsertIterator result) const
1441{
1442 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1443
1444 const QQmlJS::SourceLocation operatorLocation = regions[OperatorTokenRegion];
1445
1446 if (beforeLocation(ctx: positionInfo, right: operatorLocation)) {
1447 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1448 return;
1449 }
1450 if (afterLocation(left: operatorLocation, positionInfo)) {
1451 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1452 return;
1453 }
1454}
1455
1456/*!
1457\internal
1458Doing completion in variable declarations requires taking a look at all different cases:
1459
1460\list
1461 \li Normal variable names, like \c{let helloWorld = 123;}
1462 In this case, only autocomplete scriptexpressionidentifiers after the '=' token.
1463 Do not propose existing names for the variable name, because the variable name needs to be
1464 an identifier that is not used anywhere (to avoid shadowing and confusing code),
1465
1466 \li Deconstructed arrays, like \c{let [ helloWorld, ] = [ 123, ];}
1467 In this case, only autocomplete scriptexpressionidentifiers after the '=' token.
1468 Do not propose already existing identifiers inside the left hand side array.
1469
1470 \li Deconstructed arrays with initializers, like \c{let [ helloWorld = someVar, ] = [ 123, ];}
1471 Note: this assigns the value of someVar to helloWorld if the right hand side's first element
1472 is undefined or does not exist.
1473
1474 In this case, only autocomplete scriptexpressionidentifiers after the '=' tokens.
1475 Only propose already existing identifiers inside the left hand side array when behind a '='
1476 token.
1477
1478 \li Deconstructed Objects, like \c{let { helloWorld, } = { helloWorld: 123, };}
1479 In this case, only autocomplete scriptexpressionidentifiers after the '=' token.
1480 Do not propose already existing identifiers inside the left hand side object.
1481
1482 \li Deconstructed Objects with initializers, like \c{let { helloWorld = someVar, } = {};}
1483 Note: this assigns the value of someVar to helloWorld if the right hand side's object does
1484 not have a property called 'helloWorld'.
1485
1486 In this case, only autocomplete scriptexpressionidentifiers after the '=' token.
1487 Only propose already existing identifiers inside the left hand side object when behind a '='
1488 token.
1489
1490 \li Finally, you are allowed to nest and combine all above possibilities together for all your
1491 deconstruction needs, so the exact same completion needs to be done for
1492 DomType::ScriptPatternElement too.
1493
1494\endlist
1495*/
1496void QQmlLSCompletion::insideScriptPattern(const DomItem &parentForContext,
1497 const QQmlLSCompletionPosition &positionInfo,
1498 BackInsertIterator result) const
1499{
1500 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1501
1502 const QQmlJS::SourceLocation equal = regions[EqualTokenRegion];
1503
1504 if (!afterLocation(left: equal, positionInfo))
1505 return;
1506
1507 // otherwise, only complete case and default
1508 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1509}
1510
1511/*!
1512\internal
1513See comment on insideScriptPattern().
1514*/
1515void QQmlLSCompletion::insideVariableDeclarationEntry(const DomItem &parentForContext,
1516 const QQmlLSCompletionPosition &positionInfo,
1517 BackInsertIterator result) const
1518{
1519 insideScriptPattern(parentForContext, positionInfo, result);
1520}
1521
1522void QQmlLSCompletion::insideThrowStatement(const DomItem &parentForContext,
1523 const QQmlLSCompletionPosition &positionInfo,
1524 BackInsertIterator result) const
1525{
1526 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1527
1528 const QQmlJS::SourceLocation throwKeyword = regions[ThrowKeywordRegion];
1529
1530 if (afterLocation(left: throwKeyword, positionInfo)) {
1531 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1532 return;
1533 }
1534}
1535
1536void QQmlLSCompletion::insideLabelledStatement(const DomItem &parentForContext,
1537 const QQmlLSCompletionPosition &positionInfo,
1538 BackInsertIterator result) const
1539{
1540 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1541
1542 const QQmlJS::SourceLocation colon = regions[ColonTokenRegion];
1543
1544 if (afterLocation(left: colon, positionInfo)) {
1545 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1546 return;
1547 }
1548 // note: the case "beforeLocation(ctx, colon)" probably never happens:
1549 // this is because without the colon, the parser will probably not parse this as a
1550 // labelledstatement but as a normal expression statement.
1551 // So this case only happens when the colon already exists, and the user goes back to the
1552 // label name and requests completion for that label.
1553}
1554
1555/*!
1556\internal
1557Collect the current set of labels that some DomItem can jump to.
1558*/
1559static void collectLabels(const DomItem &context, QQmlLSCompletion::BackInsertIterator result)
1560{
1561 for (DomItem current = context; current; current = current.directParent()) {
1562 if (current.internalKind() == DomType::ScriptLabelledStatement) {
1563 const QString label = current.field(name: Fields::label).value().toString();
1564 if (label.isEmpty())
1565 continue;
1566 CompletionItem item;
1567 item.label = label.toUtf8();
1568 item.kind = int(CompletionItemKind::Value); // variable?
1569 // TODO: more stuff here?
1570 result = item;
1571 } else if (current.internalKind() == DomType::ScriptExpression) {
1572 // quick exit when leaving the JS part
1573 return;
1574 }
1575 }
1576 return;
1577}
1578
1579void QQmlLSCompletion::insideContinueStatement(const DomItem &parentForContext,
1580 const QQmlLSCompletionPosition &positionInfo,
1581 BackInsertIterator result) const
1582{
1583 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1584
1585 const QQmlJS::SourceLocation continueKeyword = regions[ContinueKeywordRegion];
1586
1587 if (afterLocation(left: continueKeyword, positionInfo)) {
1588 collectLabels(context: parentForContext, result);
1589 return;
1590 }
1591}
1592
1593void QQmlLSCompletion::insideBreakStatement(const DomItem &parentForContext,
1594 const QQmlLSCompletionPosition &positionInfo,
1595 BackInsertIterator result) const
1596{
1597 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1598
1599 const QQmlJS::SourceLocation breakKeyword = regions[BreakKeywordRegion];
1600
1601 if (afterLocation(left: breakKeyword, positionInfo)) {
1602 collectLabels(context: parentForContext, result);
1603 return;
1604 }
1605}
1606
1607void QQmlLSCompletion::insideConditionalExpression(const DomItem &parentForContext,
1608 const QQmlLSCompletionPosition &positionInfo,
1609 BackInsertIterator result) const
1610{
1611 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1612
1613 const QQmlJS::SourceLocation questionMark = regions[QuestionMarkTokenRegion];
1614 const QQmlJS::SourceLocation colon = regions[ColonTokenRegion];
1615
1616 if (beforeLocation(ctx: positionInfo, right: questionMark)) {
1617 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1618 return;
1619 }
1620 if (betweenLocations(left: questionMark, positionInfo, right: colon)) {
1621 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1622 return;
1623 }
1624 if (afterLocation(left: colon, positionInfo)) {
1625 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1626 return;
1627 }
1628}
1629
1630void QQmlLSCompletion::insideUnaryExpression(const DomItem &parentForContext,
1631 const QQmlLSCompletionPosition &positionInfo,
1632 BackInsertIterator result) const
1633{
1634 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1635
1636 const QQmlJS::SourceLocation operatorToken = regions[OperatorTokenRegion];
1637
1638 if (afterLocation(left: operatorToken, positionInfo)) {
1639 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1640 return;
1641 }
1642}
1643
1644void QQmlLSCompletion::insidePostExpression(const DomItem &parentForContext,
1645 const QQmlLSCompletionPosition &positionInfo,
1646 BackInsertIterator result) const
1647{
1648 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1649
1650 const QQmlJS::SourceLocation operatorToken = regions[OperatorTokenRegion];
1651
1652 if (beforeLocation(ctx: positionInfo, right: operatorToken)) {
1653 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1654 return;
1655 }
1656}
1657
1658void QQmlLSCompletion::insideParenthesizedExpression(const DomItem &parentForContext,
1659 const QQmlLSCompletionPosition &positionInfo,
1660 BackInsertIterator result) const
1661{
1662 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1663
1664 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1665 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1666
1667 if (betweenLocations(left: leftParenthesis, positionInfo, right: rightParenthesis)) {
1668 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1669 return;
1670 }
1671}
1672
1673void QQmlLSCompletion::insideTemplateLiteral(const DomItem &parentForContext,
1674 const QQmlLSCompletionPosition &positionInfo,
1675 BackInsertIterator result) const
1676{
1677 Q_UNUSED(parentForContext);
1678 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1679}
1680
1681void QQmlLSCompletion::insideNewExpression(const DomItem &parentForContext,
1682 const QQmlLSCompletionPosition &positionInfo,
1683 BackInsertIterator result) const
1684{
1685 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1686 const QQmlJS::SourceLocation newKeyword = regions[NewKeywordRegion];
1687
1688 if (afterLocation(left: newKeyword, positionInfo)) {
1689 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1690 return;
1691 }
1692}
1693
1694void QQmlLSCompletion::insideNewMemberExpression(const DomItem &parentForContext,
1695 const QQmlLSCompletionPosition &positionInfo,
1696 BackInsertIterator result) const
1697{
1698 const auto regions = FileLocations::treeOf(parentForContext)->info().regions;
1699 const QQmlJS::SourceLocation newKeyword = regions[NewKeywordRegion];
1700 const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion];
1701 const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion];
1702
1703 if (betweenLocations(left: newKeyword, positionInfo, right: leftParenthesis)
1704 || betweenLocations(left: leftParenthesis, positionInfo, right: rightParenthesis)) {
1705 suggestJSExpressionCompletion(scriptIdentifier: positionInfo.itemAtPosition, result);
1706 return;
1707 }
1708}
1709
1710void QQmlLSCompletion::signalHandlerCompletion(const QQmlJSScope::ConstPtr &scope,
1711 QDuplicateTracker<QString> *usedNames,
1712 BackInsertIterator result) const
1713{
1714 const auto keyValues = scope->methods().asKeyValueRange();
1715 for (const auto &[name, method] : keyValues) {
1716 if (method.access() != QQmlJSMetaMethod::Public
1717 || method.methodType() != QQmlJSMetaMethodType::Signal) {
1718 continue;
1719 }
1720 if (usedNames && usedNames->hasSeen(s: name)) {
1721 continue;
1722 }
1723
1724 CompletionItem completion;
1725 completion.label = QQmlSignalNames::signalNameToHandlerName(signal: name).toUtf8();
1726 completion.kind = int(CompletionItemKind::Method);
1727 result = completion;
1728 }
1729}
1730
1731/*!
1732\internal
1733Decide which completions can be used at currentItem and compute them.
1734*/
1735QList<CompletionItem>
1736QQmlLSCompletion::completions(const DomItem &currentItem,
1737 const CompletionContextStrings &contextStrings) const
1738{
1739 QList<CompletionItem> result;
1740 collectCompletions(currentItem, ctx: contextStrings, result: std::back_inserter(x&: result));
1741 return result;
1742}
1743
1744void QQmlLSCompletion::collectCompletions(const DomItem &currentItem,
1745 const CompletionContextStrings &contextStrings,
1746 BackInsertIterator result) const
1747{
1748 /*!
1749 Completion is not provided on a script identifier expression because script identifier
1750 expressions lack context information. Instead, find the first parent that has enough
1751 context information and provide completion for this one.
1752 For example, a script identifier expression \c{le} in
1753 \badcode
1754 for (;le;) { ... }
1755 \endcode
1756 will get completion for a property called \c{leProperty}, while the same script identifier
1757 expression in
1758 \badcode
1759 for (le;;) { ... }
1760 \endcode
1761 will, in addition to \c{leProperty}, also get completion for the \c{let} statement snippet.
1762 In this example, the parent used for the completion is the for-statement, of type
1763 DomType::ScriptForStatement.
1764
1765 In addition of the parent for the context, use positionInfo to have exact information on where
1766 the cursor is (to compare with the SourceLocations of tokens) and which item is at this position
1767 (required to provide completion at the correct position, for example for attached properties).
1768 */
1769 const QQmlLSCompletionPosition positionInfo{ .itemAtPosition: currentItem, .cursorPosition: contextStrings };
1770 for (DomItem currentParent = currentItem; currentParent;
1771 currentParent = currentParent.directParent()) {
1772 const DomType currentType = currentParent.internalKind();
1773
1774 switch (currentType) {
1775 case DomType::Id:
1776 // suppress completions for ids
1777 return;
1778 case DomType::Pragma:
1779 insidePragmaCompletion(currentItem: currentParent, positionInfo, result);
1780 return;
1781 case DomType::ScriptType: {
1782 if (currentParent.directParent().internalKind() == DomType::QmlObject) {
1783 insideQmlObjectCompletion(parentForContext: currentParent.directParent(), positionInfo, result);
1784 return;
1785 }
1786
1787 LocalSymbolsTypes options;
1788 options.setFlag(flag: LocalSymbolsType::ObjectType);
1789 options.setFlag(flag: LocalSymbolsType::ValueType);
1790 suggestReachableTypes(el: currentItem, options, kind: CompletionItemKind::Class, it: result);
1791 return;
1792 }
1793 case DomType::ScriptFormalParameter:
1794 // no autocompletion inside of function parameter definition
1795 return;
1796 case DomType::Binding:
1797 insideBindingCompletion(currentItem: currentParent, positionInfo, result);
1798 return;
1799 case DomType::Import:
1800 insideImportCompletion(currentItem: currentParent, positionInfo, result);
1801 return;
1802 case DomType::ScriptForStatement:
1803 insideForStatementCompletion(parentForContext: currentParent, positionInfo, result);
1804 return;
1805 case DomType::ScriptBlockStatement:
1806 suggestJSStatementCompletion(itemAtPosition: positionInfo.itemAtPosition, result);
1807 return;
1808 case DomType::QmlFile:
1809 insideQmlFileCompletion(currentItem: currentParent, positionInfo, result);
1810 return;
1811 case DomType::QmlObject:
1812 insideQmlObjectCompletion(parentForContext: currentParent, positionInfo, result);
1813 return;
1814 case DomType::MethodInfo:
1815 // suppress completions
1816 return;
1817 case DomType::PropertyDefinition:
1818 insidePropertyDefinitionCompletion(currentItem: currentParent, positionInfo, result);
1819 return;
1820 case DomType::ScriptBinaryExpression:
1821 // ignore field member expressions: these need additional context from its parents
1822 if (QQmlLSUtils::isFieldMemberExpression(item: currentParent))
1823 continue;
1824 insideBinaryExpressionCompletion(parentForContext: currentParent, positionInfo, result);
1825 return;
1826 case DomType::ScriptLiteral:
1827 insideScriptLiteralCompletion(currentItem: currentParent, positionInfo, result);
1828 return;
1829 case DomType::ScriptRegExpLiteral:
1830 // no completion inside of regexp literals
1831 return;
1832 case DomType::ScriptCallExpression:
1833 insideCallExpression(currentItem: currentParent, positionInfo, result);
1834 return;
1835 case DomType::ScriptIfStatement:
1836 insideIfStatement(currentItem: currentParent, positionInfo, result);
1837 return;
1838 case DomType::ScriptReturnStatement:
1839 insideReturnStatement(currentItem: currentParent, positionInfo, result);
1840 return;
1841 case DomType::ScriptWhileStatement:
1842 insideWhileStatement(currentItem: currentParent, positionInfo, result);
1843 return;
1844 case DomType::ScriptDoWhileStatement:
1845 insideDoWhileStatement(parentForContext: currentParent, positionInfo, result);
1846 return;
1847 case DomType::ScriptForEachStatement:
1848 insideForEachStatement(parentForContext: currentParent, positionInfo, result);
1849 return;
1850 case DomType::ScriptTryCatchStatement:
1851 /*!
1852 \internal
1853 The Ecmascript standard specifies that there can only be a block statement between \c
1854 try and \c catch(...), \c try and \c finally and \c catch(...) and \c finally, so all of
1855 these completions are already handled by the DomType::ScriptBlockStatement completion.
1856 The only place in the try statement where there is no BlockStatement and therefore needs
1857 its own completion is inside the catch parameter, but that is
1858 \quotation
1859 An optional identifier or pattern to hold the caught exception for the associated catch
1860 block.
1861 \endquotation
1862 citing
1863 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch?retiredLocale=de#exceptionvar.
1864 This means that no completion is needed inside a catch-expression, as it should contain
1865 an identifier that is not yet used anywhere.
1866 Therefore, no completion is required at all when inside a try-statement but outside a
1867 block-statement.
1868 */
1869 return;
1870 case DomType::ScriptSwitchStatement:
1871 insideSwitchStatement(parentForContext: currentParent, positionInfo, result);
1872 return;
1873 case DomType::ScriptCaseClause:
1874 insideCaseClause(parentForContext: currentParent, positionInfo, result);
1875 return;
1876 case DomType::ScriptDefaultClause:
1877 if (ctxBeforeStatement(positionInfo, parentForContext: currentParent, firstRegion: QQmlJS::Dom::DefaultKeywordRegion))
1878 continue;
1879 insideDefaultClause(parentForContext: currentParent, positionInfo, result);
1880 return;
1881 case DomType::ScriptCaseBlock:
1882 insideCaseBlock(parentForContext: currentParent, positionInfo, result);
1883 return;
1884 case DomType::ScriptVariableDeclaration:
1885 // not needed: thats a list of ScriptVariableDeclarationEntry, and those entries cannot
1886 // be suggested because they all start with `{`, `[` or an identifier that should not be
1887 // in use yet.
1888 return;
1889 case DomType::ScriptVariableDeclarationEntry:
1890 insideVariableDeclarationEntry(parentForContext: currentParent, positionInfo, result);
1891 return;
1892 case DomType::ScriptProperty:
1893 // fallthrough: a ScriptProperty is a ScriptPattern but inside a JS Object. It gets the
1894 // same completions as a ScriptPattern.
1895 case DomType::ScriptPattern:
1896 insideScriptPattern(parentForContext: currentParent, positionInfo, result);
1897 return;
1898 case DomType::ScriptThrowStatement:
1899 insideThrowStatement(parentForContext: currentParent, positionInfo, result);
1900 return;
1901 case DomType::ScriptLabelledStatement:
1902 insideLabelledStatement(parentForContext: currentParent, positionInfo, result);
1903 return;
1904 case DomType::ScriptContinueStatement:
1905 insideContinueStatement(parentForContext: currentParent, positionInfo, result);
1906 return;
1907 case DomType::ScriptBreakStatement:
1908 insideBreakStatement(parentForContext: currentParent, positionInfo, result);
1909 return;
1910 case DomType::ScriptConditionalExpression:
1911 insideConditionalExpression(parentForContext: currentParent, positionInfo, result);
1912 return;
1913 case DomType::ScriptUnaryExpression:
1914 insideUnaryExpression(parentForContext: currentParent, positionInfo, result);
1915 return;
1916 case DomType::ScriptPostExpression:
1917 insidePostExpression(parentForContext: currentParent, positionInfo, result);
1918 return;
1919 case DomType::ScriptParenthesizedExpression:
1920 insideParenthesizedExpression(parentForContext: currentParent, positionInfo, result);
1921 return;
1922 case DomType::ScriptTemplateLiteral:
1923 insideTemplateLiteral(parentForContext: currentParent, positionInfo, result);
1924 return;
1925 case DomType::ScriptTemplateStringPart:
1926 // no completion inside of the non-expression parts of template strings
1927 return;
1928 case DomType::ScriptNewExpression:
1929 insideNewExpression(parentForContext: currentParent, positionInfo, result);
1930 return;
1931 case DomType::ScriptNewMemberExpression:
1932 insideNewMemberExpression(parentForContext: currentParent, positionInfo, result);
1933 return;
1934 case DomType::ScriptThisExpression:
1935 // suppress completions on `this`
1936 return;
1937 case DomType::ScriptSuperLiteral:
1938 // suppress completions on `super`
1939 return;
1940
1941 // TODO: Implement those statements.
1942 // In the meanwhile, suppress completions to avoid weird behaviors.
1943 case DomType::ScriptArray:
1944 case DomType::ScriptObject:
1945 case DomType::ScriptElision:
1946 case DomType::ScriptArrayEntry:
1947 return;
1948
1949 default:
1950 continue;
1951 }
1952 Q_UNREACHABLE();
1953 }
1954
1955 // no completion could be found
1956 qCDebug(QQmlLSUtilsLog) << "No completion was found for current request.";
1957 return;
1958}
1959
1960QQmlLSCompletion::QQmlLSCompletion(const QFactoryLoader &pluginLoader)
1961{
1962 const auto keys = pluginLoader.metaDataKeys();
1963 for (qsizetype i = 0; i < keys.size(); ++i) {
1964 auto instance = std::unique_ptr<QQmlLSPlugin>(
1965 qobject_cast<QQmlLSPlugin *>(object: pluginLoader.instance(index: i)));
1966 if (!instance)
1967 continue;
1968 if (auto completionInstance = instance->createCompletionPlugin())
1969 m_plugins.push_back(x: std::move(completionInstance));
1970 }
1971}
1972
1973/*!
1974\internal
1975Helper method to call a method on all loaded plugins.
1976*/
1977void QQmlLSCompletion::collectFromPlugins(qxp::function_ref<CompletionFromPluginFunction> f,
1978 BackInsertIterator result) const
1979{
1980 for (const auto &plugin : m_plugins) {
1981 Q_ASSERT(plugin);
1982 f(plugin.get(), result);
1983 }
1984}
1985
1986void QQmlLSCompletion::suggestSnippetsForLeftHandSideOfBinding(const DomItem &itemAtPosition,
1987 BackInsertIterator result) const
1988{
1989 collectFromPlugins(
1990 f: [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) {
1991 p->suggestSnippetsForLeftHandSideOfBinding(items: itemAtPosition, result);
1992 },
1993 result);
1994}
1995
1996void QQmlLSCompletion::suggestSnippetsForRightHandSideOfBinding(const DomItem &itemAtPosition,
1997 BackInsertIterator result) const
1998{
1999 collectFromPlugins(
2000 f: [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) {
2001 p->suggestSnippetsForRightHandSideOfBinding(items: itemAtPosition, result);
2002 },
2003 result);
2004}
2005
2006QT_END_NAMESPACE
2007

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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