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 | |
6 | using namespace QLspSpecification; |
7 | using namespace QQmlJS::Dom; |
8 | using namespace Qt::StringLiterals; |
9 | |
10 | QT_BEGIN_NAMESPACE |
11 | |
12 | Q_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 | |
19 | Use the \l{completions} method to obtain completions at a certain DomItem. |
20 | |
21 | All the other methods in this class are helper methods: some compute completions for specific QML |
22 | and JS constructs and some are shared between multiple QML or JS constructs to avoid code |
23 | duplication. Most of the helper methods add their completion items via a BackInsertIterator. |
24 | |
25 | Some helper methods are called "suggest*" and will try to suggest code that does not exist yet. For |
26 | example, any JS statement can be expected inside a Blockstatement so suggestJSStatementCompletion() |
27 | is used to suggest JS statements inside of BlockStatements. Another example might be |
28 | suggestReachableTypes() that will suggest Types for type annotations, attached types or Qml Object |
29 | hierarchies, or suggestCaseAndDefaultStatementCompletion() that will only suggest "case" and |
30 | "default" clauses for switch statements. |
31 | |
32 | Some helper methods are called "inside*" and will try to suggest code inside an existing structure. |
33 | For example, insideForStatementCompletion() will try to suggest completion for the different code |
34 | pieces initializer, condition, increment and statement that exist inside of: |
35 | \badcode |
36 | for(initializer; condition; increment) |
37 | statement |
38 | \endcode |
39 | */ |
40 | |
41 | CompletionItem 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 | |
63 | CompletionItem 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 | |
72 | Statements and expressions need to provide different completions depending on where the cursor is. |
73 | For example, lets take following for-statement: |
74 | \badcode |
75 | for (let i = 0; <here> ; ++i) {} |
76 | \endcode |
77 | We want to provide script expression completion (method names, property names, available JS |
78 | variables names, QML objects ids, and so on) at the place denoted by \c{<here>}. |
79 | The question is: how do we know that the cursor is really at \c{<here>}? In the case of the |
80 | for-loop, we can compare the position of the cursor with the first and the second semicolon of the |
81 | for loop. |
82 | |
83 | If the first semicolon does not exist, it has an invalid sourcelocation and the cursor is |
84 | definitively \e{not} at \c{<here>}. Therefore, return false when \c{left} is invalid. |
85 | |
86 | If the second semicolon does not exist, then just ignore it: it might not have been written yet. |
87 | */ |
88 | bool 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 |
106 | Returns true if ctx denotes an offset lying behind left.end(), and false otherwise. |
107 | */ |
108 | bool QQmlLSCompletion::afterLocation(QQmlJS::SourceLocation left, |
109 | const QQmlLSCompletionPosition &positionInfo) const |
110 | { |
111 | return betweenLocations(left, positionInfo, right: QQmlJS::SourceLocation{}); |
112 | } |
113 | |
114 | /*! |
115 | \internal |
116 | Returns true if ctx denotes an offset lying before right.begin(), and false otherwise. |
117 | */ |
118 | bool 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 | |
131 | bool 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 | |
140 | void |
141 | QQmlLSCompletion::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 | |
168 | void 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 | |
256 | void 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 | |
267 | static 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 |
290 | Obtain the types reachable from \c{el} as a CompletionItems. |
291 | */ |
292 | void 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 | |
325 | void 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 | |
350 | void 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 | |
370 | void 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 | |
390 | void 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 | |
405 | void 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 |
418 | Creates completion items for enumerationvalues. |
419 | If enumeratorName is a valid enumerator then only do completion for the requested enumerator, and |
420 | otherwise do completion for \b{all other possible} enumerators. |
421 | |
422 | For example: |
423 | ``` |
424 | id: someItem |
425 | enum Hello { World } |
426 | enum MyEnum { ValueOne, ValueTwo } |
427 | |
428 | // Hello does refer to a enumerator: |
429 | property var a: Hello.<complete only World here> |
430 | |
431 | // someItem does not refer to a enumerator: |
432 | property var b: someItem.<complete World, ValueOne and ValueTwo here> |
433 | ``` |
434 | */ |
435 | |
436 | void 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 |
453 | Calls F on all JavaScript-parents of scope. For example, you can use this method to |
454 | collect 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 | */ |
464 | template<typename F> |
465 | void 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 |
476 | Suggest enumerations (if applicable) and enumeration values from \c scope, for example \c |
477 | Asynchronous from the \c CompilationMode enum: |
478 | |
479 | \qml |
480 | property var xxx: Component.Asynchronous // Component contains the \c CompilationMode enum |
481 | property var xxx2: CompilationMode.Asynchronous |
482 | \endqml |
483 | */ |
484 | void 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 | |
499 | Returns the owner of a qualified expression for further resolving, for example: |
500 | 1. \c owner from the \c member ScriptExpression in \c {owner.member}. This happens when completion |
501 | is requested on \c member. |
502 | 2. \c owner from the ScriptBinaryExpression \c {owner.member}. This happens when completion is |
503 | requested on the dot between \c owner and \c member. |
504 | 3. An empty DomItem otherwise. |
505 | */ |
506 | DomItem 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 |
526 | Generate autocompletions for JS expressions, suggest possible properties, methods, etc. |
527 | |
528 | If 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 |
530 | from the correct type. For the previous example that would be properties, methods, etc. from the |
531 | Component attached type. |
532 | */ |
533 | void 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 | |
630 | static 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 | |
644 | bool 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 | |
651 | bool QQmlLSCompletion::cursorAfterColon(const DomItem ¤tItem, |
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 | |
670 | This mapping of pragma names to pragma values is not complete. In fact, it only contains the |
671 | pragma names and values that one should see autocompletion for. |
672 | Some pragmas like FunctionSignatureBehavior or Strict or the Reference/Value of ValueTypeBehavior, |
673 | for example, should currently not be proposed as completion items by qmlls. |
674 | |
675 | An empty QList-value in the QMap means that the pragma does not accept pragma values. |
676 | */ |
677 | static 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 | |
685 | void 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 | |
715 | void 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 | |
810 | void QQmlLSCompletion::insidePropertyDefinitionCompletion( |
811 | const DomItem ¤tItem, 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 | |
872 | void QQmlLSCompletion::insideBindingCompletion(const DomItem ¤tItem, |
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 | |
916 | void QQmlLSCompletion::insideImportCompletion(const DomItem ¤tItem, |
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 | |
930 | void QQmlLSCompletion::insideQmlFileCompletion(const DomItem ¤tItem, |
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 |
954 | Generate the snippets for let, var and const variable declarations. |
955 | */ |
956 | void 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 |
973 | Generate the snippets for case and default statements. |
974 | */ |
975 | void 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 |
990 | Break 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 | */ |
997 | void 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 |
1059 | Generates snippets or keywords for all possible JS statements where it makes sense. To use whenever |
1060 | any JS statement can be expected, but when no JS statement is there yet. |
1061 | |
1062 | Only generates JS expression completions when itemAtPosition is a qualified name. |
1063 | |
1064 | Here 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 |
1067 | bracket anyway. \li EmptyStatement completion would only generate a single \c{;} \li |
1068 | ExpressionStatement completion cannot generate any snippet, only identifiers \li WithStatement |
1069 | completion is not recommended: qmllint will warn about usage of with statements \li |
1070 | LabelledStatement completion might need to propose labels (TODO?) \li DebuggerStatement completion |
1071 | does not strike as being very useful \endlist |
1072 | */ |
1073 | void 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 | |
1153 | void 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 | |
1182 | void QQmlLSCompletion::insideScriptLiteralCompletion(const DomItem ¤tItem, |
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 | |
1193 | void QQmlLSCompletion::insideCallExpression(const DomItem ¤tItem, |
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 | |
1210 | void QQmlLSCompletion::insideIfStatement(const DomItem ¤tItem, |
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 | |
1233 | void QQmlLSCompletion::insideReturnStatement(const DomItem ¤tItem, |
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 | |
1246 | void QQmlLSCompletion::insideWhileStatement(const DomItem ¤tItem, |
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 | |
1264 | void 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 | |
1284 | void 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 | |
1310 | void 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 | |
1325 | void 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 |
1347 | Checks if a case or default clause does happen before ctx in the code. |
1348 | */ |
1349 | bool QQmlLSCompletion::isCaseOrDefaultBeforeCtx(const DomItem ¤tClause, |
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 | |
1369 | Search for a `case ...:` or a `default: ` clause happening before ctx, and return the |
1370 | corresponding DomItem of type DomType::CaseClauses or DomType::DefaultClause. |
1371 | |
1372 | Return an empty DomItem if neither case nor default was found. |
1373 | */ |
1374 | DomItem |
1375 | QQmlLSCompletion::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 | |
1401 | void 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 | |
1424 | void 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 | |
1438 | void 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 |
1458 | Doing 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 | */ |
1496 | void 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 |
1513 | See comment on insideScriptPattern(). |
1514 | */ |
1515 | void QQmlLSCompletion::insideVariableDeclarationEntry(const DomItem &parentForContext, |
1516 | const QQmlLSCompletionPosition &positionInfo, |
1517 | BackInsertIterator result) const |
1518 | { |
1519 | insideScriptPattern(parentForContext, positionInfo, result); |
1520 | } |
1521 | |
1522 | void 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 | |
1536 | void 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 |
1557 | Collect the current set of labels that some DomItem can jump to. |
1558 | */ |
1559 | static 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 | |
1579 | void 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 | |
1593 | void 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 | |
1607 | void 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 | |
1630 | void 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 | |
1644 | void 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 | |
1658 | void 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 | |
1673 | void 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 | |
1681 | void 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 | |
1694 | void 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 | |
1710 | void 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 |
1733 | Decide which completions can be used at currentItem and compute them. |
1734 | */ |
1735 | QList<CompletionItem> |
1736 | QQmlLSCompletion::completions(const DomItem ¤tItem, |
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 | |
1744 | void QQmlLSCompletion::collectCompletions(const DomItem ¤tItem, |
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 | |
1960 | QQmlLSCompletion::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 |
1975 | Helper method to call a method on all loaded plugins. |
1976 | */ |
1977 | void 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 | |
1986 | void 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 | |
1996 | void 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 | |
2006 | QT_END_NAMESPACE |
2007 |
Definitions
- QQmlLSCompletionLog
- makeSnippet
- makeSnippet
- betweenLocations
- afterLocation
- beforeLocation
- ctxBeforeStatement
- suggestBindingCompletion
- insideImportCompletionHelper
- idsCompletions
- testScopeSymbol
- suggestReachableTypes
- jsIdentifierCompletion
- methodCompletion
- propertyCompletion
- enumerationCompletion
- enumerationValueCompletionHelper
- enumerationValueCompletion
- collectFromAllJavaScriptParents
- suggestEnumerationsAndEnumerationValues
- ownerOfQualifiedExpression
- suggestJSExpressionCompletion
- resolve
- cursorInFrontOfItem
- cursorAfterColon
- valuesForPragmas
- insidePragmaCompletion
- insideQmlObjectCompletion
- insidePropertyDefinitionCompletion
- insideBindingCompletion
- insideImportCompletion
- insideQmlFileCompletion
- suggestVariableDeclarationStatementCompletion
- suggestCaseAndDefaultStatementCompletion
- suggestContinueAndBreakStatementIfNeeded
- suggestJSStatementCompletion
- insideForStatementCompletion
- insideScriptLiteralCompletion
- insideCallExpression
- insideIfStatement
- insideReturnStatement
- insideWhileStatement
- insideDoWhileStatement
- insideForEachStatement
- insideSwitchStatement
- insideCaseClause
- isCaseOrDefaultBeforeCtx
- previousCaseOfCaseBlock
- insideCaseBlock
- insideDefaultClause
- insideBinaryExpressionCompletion
- insideScriptPattern
- insideVariableDeclarationEntry
- insideThrowStatement
- insideLabelledStatement
- collectLabels
- insideContinueStatement
- insideBreakStatement
- insideConditionalExpression
- insideUnaryExpression
- insidePostExpression
- insideParenthesizedExpression
- insideTemplateLiteral
- insideNewExpression
- insideNewMemberExpression
- signalHandlerCompletion
- completions
- collectCompletions
- QQmlLSCompletion
- collectFromPlugins
- suggestSnippetsForLeftHandSideOfBinding
Start learning QML with our Intro Training
Find out more