Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmlsemantictokens.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include <qqmlsemantictokens_p.h>
6
7#include <QtQmlLS/private/qqmllsutils_p.h>
8#include <QtQmlDom/private/qqmldomscriptelements_p.h>
9#include <QtQmlDom/private/qqmldomfieldfilter_p.h>
10
11#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
12
14
15Q_LOGGING_CATEGORY(semanticTokens, "qt.languageserver.semanticTokens")
16
17using namespace QQmlJS::AST;
18using namespace QQmlJS::Dom;
19using namespace QLspSpecification;
20
21namespace QmlHighlighting {
22
24{
25 switch (highlightKind) {
83 default:
85 }
86}
87
88static int mapToProtocolDefault(QmlHighlightKind highlightKind)
89{
90 switch (highlightKind) {
144 default:
146 }
147}
148
149/*!
150\internal
151\brief Further resolves the type of a JavaScriptIdentifier
152A global object can be in the object form or in the function form.
153For example, Date can be used as a constructor function (like new Date())
154or as a object (like Date.now()).
155*/
157 const QString &name)
158{
159 // Some objects are not constructable, they are always objects.
160 static QSet<QString> noConstructorObjects = { u"Math"_s, u"JSON"_s, u"Atomics"_s, u"Reflect"_s,
161 u"console"_s };
162 // if the method name is in the list of noConstructorObjects, then it is a global object. Do not
163 // perform further checks.
164 if (noConstructorObjects.contains(name))
166 // Check if the method is called with new, then it is a constructor function
167 if (item.directParent().internalKind() == DomType::ScriptNewMemberExpression) {
169 }
170 if (DomItem containingCallExpression = item.filterUp(
171 [](DomType k, const DomItem &) { return k == DomType::ScriptCallExpression; },
172 FilterUpOptions::ReturnOuter)) {
173 // Call expression
174 // if callee is binary expression, then the rightest part is the method name
175 const auto callee = containingCallExpression.field(Fields::callee);
176 if (callee.internalKind() == DomType::ScriptBinaryExpression) {
177 const auto right = callee.field(Fields::right);
178 if (right.internalKind() == DomType::ScriptIdentifierExpression
179 && right.field(Fields::identifier).value().toString() == name) {
181 } else {
183 }
184 } else {
186 }
187 }
188 return std::nullopt;
189}
190
191static int fromQmlModifierKindToLspTokenType(QmlHighlightModifiers highlightModifier)
192{
193 using namespace QLspSpecification;
194 using namespace Utils;
195 int modifier = 0;
196
197 if (highlightModifier.testFlag(QmlHighlightModifier::QmlPropertyDefinition))
198 addModifier(SemanticTokenModifiers::Definition, &modifier);
199
200 if (highlightModifier.testFlag(QmlHighlightModifier::QmlDefaultProperty))
201 addModifier(SemanticTokenModifiers::DefaultLibrary, &modifier);
202
203 if (highlightModifier.testFlag(QmlHighlightModifier::QmlFinalProperty))
204 addModifier(SemanticTokenModifiers::Static, &modifier);
205
206 if (highlightModifier.testFlag(QmlHighlightModifier::QmlRequiredProperty))
207 addModifier(SemanticTokenModifiers::Abstract, &modifier);
208
209 if (highlightModifier.testFlag(QmlHighlightModifier::QmlReadonlyProperty))
210 addModifier(SemanticTokenModifiers::Readonly, &modifier);
211
212 return modifier;
213}
214
216{
217 QMultiMap<QString, QString> fieldFilterAdd{};
218 QMultiMap<QString, QString> fieldFilterRemove{
219 { QString(), Fields::propertyInfos.toString() },
220 { QString(), Fields::fileLocationsTree.toString() },
221 { QString(), Fields::importScope.toString() },
222 { QString(), Fields::defaultPropertyName.toString() },
223 { QString(), Fields::get.toString() },
224 };
225 return FieldFilter{ fieldFilterAdd, fieldFilterRemove };
226}
227
228HighlightToken::HighlightToken(const QQmlJS::SourceLocation &loc,
229 QmlHighlightKind kind,
230 QmlHighlightModifiers modifiers)
231 : loc(loc), kind(kind), modifiers(modifiers)
232{
233}
234
235HighlightingVisitor::HighlightingVisitor(const QQmlJS::Dom::DomItem &item,
236 const std::optional<HighlightsRange> &range)
237 : m_range(range)
238{
239 item.visitTree(
240 Path(),
241 [this](const Path &path, const DomItem &item, bool b) {
242 return this->visitor(path, item, b);
243 },
244 VisitOption::Default | VisitOption::NoPath, emptyChildrenVisitor, emptyChildrenVisitor,
245 highlightingFilter());
246}
247
248bool HighlightingVisitor::visitor(Path, const DomItem &item, bool)
249{
250 if (m_range.has_value()) {
251 const auto fLocs = FileLocations::treeOf(item);
252 if (!fLocs)
253 return true;
254 const auto regions = fLocs->info().regions;
255 if (!Utils::rangeOverlapsWithSourceLocation(regions[MainRegion],
256 m_range.value()))
257 return true;
258 }
259 switch (item.internalKind()) {
260 case DomType::Comment: {
261 highlightComment(item);
262 return true;
263 }
264 case DomType::Import: {
265 highlightImport(item);
266 return true;
267 }
268 case DomType::Binding: {
269 highlightBinding(item);
270 return true;
271 }
272 case DomType::Pragma: {
273 highlightPragma(item);
274 return true;
275 }
276 case DomType::EnumDecl: {
277 highlightEnumDecl(item);
278 return true;
279 }
280 case DomType::EnumItem: {
281 highlightEnumItem(item);
282 return true;
283 }
284 case DomType::QmlObject: {
285 highlightQmlObject(item);
286 return true;
287 }
288 case DomType::QmlComponent: {
289 highlightComponent(item);
290 return true;
291 }
292 case DomType::PropertyDefinition: {
293 highlightPropertyDefinition(item);
294 return true;
295 }
296 case DomType::MethodInfo: {
297 highlightMethod(item);
298 return true;
299 }
300 case DomType::ScriptLiteral: {
301 highlightScriptLiteral(item);
302 return true;
303 }
304 case DomType::ScriptCallExpression: {
305 highlightCallExpression(item);
306 return true;
307 }
308 case DomType::ScriptIdentifierExpression: {
309 highlightIdentifier(item);
310 return true;
311 }
312 default:
313 if (item.ownerAs<ScriptExpression>())
314 highlightScriptExpressions(item);
315 return true;
316 }
317 Q_UNREACHABLE_RETURN(false);
318}
319
320void HighlightingVisitor::highlightComment(const DomItem &item)
321{
322 const auto comment = item.as<Comment>();
323 Q_ASSERT(comment);
324 const auto locs = Utils::sourceLocationsFromMultiLineToken(
325 comment->info().comment(), comment->info().sourceLocation());
326 for (const auto &loc : locs)
327 addHighlight(loc, QmlHighlightKind::Comment);
328}
329
330void HighlightingVisitor::highlightImport(const DomItem &item)
331{
332 const auto fLocs = FileLocations::treeOf(item);
333 if (!fLocs)
334 return;
335 const auto regions = fLocs->info().regions;
336 const auto import = item.as<Import>();
337 Q_ASSERT(import);
338 addHighlight(regions[ImportTokenRegion], QmlHighlightKind::QmlKeyword);
339 if (import->uri.isModule())
340 addHighlight(regions[ImportUriRegion], QmlHighlightKind::QmlImportId);
341 else
342 addHighlight(regions[ImportUriRegion], QmlHighlightKind::String);
343 if (regions.contains(VersionRegion))
344 addHighlight(regions[VersionRegion], QmlHighlightKind::Number);
345 if (regions.contains(AsTokenRegion)) {
346 addHighlight(regions[AsTokenRegion], QmlHighlightKind::QmlKeyword);
347 addHighlight(regions[IdNameRegion], QmlHighlightKind::QmlNamespace);
348 }
349}
350
351void HighlightingVisitor::highlightBinding(const DomItem &item)
352{
353 const auto binding = item.as<Binding>();
354 Q_ASSERT(binding);
355 const auto fLocs = FileLocations::treeOf(item);
356 if (!fLocs) {
357 qCDebug(semanticTokens) << "Can't find the locations for" << item.internalKind();
358 return;
359 }
360 const auto regions = fLocs->info().regions;
361 // If dotted name, then defer it to be handled in ScriptIdentifierExpression
362 if (binding->name().contains("."_L1))
363 return;
364
365 if (binding->bindingType() != BindingType::Normal) {
366 addHighlight(regions[OnTokenRegion], QmlHighlightKind::QmlKeyword);
367 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlProperty);
368 return;
369 }
370
371 return addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlProperty);
372}
373
374void HighlightingVisitor::highlightPragma(const DomItem &item)
375{
376 const auto fLocs = FileLocations::treeOf(item);
377 if (!fLocs)
378 return;
379 const auto regions = fLocs->info().regions;
380 addHighlight(regions[PragmaKeywordRegion], QmlHighlightKind::QmlKeyword);
381 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlPragmaName );
382 const auto pragma = item.as<Pragma>();
383 for (auto i = 0; i < pragma->values.size(); ++i) {
384 DomItem value = item.field(Fields::values).index(i);
385 const auto valueRegions = FileLocations::treeOf(value)->info().regions;
386 addHighlight(valueRegions[PragmaValuesRegion], QmlHighlightKind::QmlPragmaValue);
387 }
388 return;
389}
390
391void HighlightingVisitor::highlightEnumDecl(const DomItem &item)
392{
393 const auto fLocs = FileLocations::treeOf(item);
394 if (!fLocs)
395 return;
396 const auto regions = fLocs->info().regions;
397 addHighlight(regions[EnumKeywordRegion], QmlHighlightKind::QmlKeyword);
398 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlEnumName);
399}
400
401void HighlightingVisitor::highlightEnumItem(const DomItem &item)
402{
403 const auto fLocs = FileLocations::treeOf(item);
404 if (!fLocs)
405 return;
406 const auto regions = fLocs->info().regions;
407 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlEnumMember);
408 if (regions.contains(EnumValueRegion))
409 addHighlight(regions[EnumValueRegion], QmlHighlightKind::Number);
410}
411
412void HighlightingVisitor::highlightQmlObject(const DomItem &item)
413{
414 const auto qmlObject = item.as<QmlObject>();
415 Q_ASSERT(qmlObject);
416 const auto fLocs = FileLocations::treeOf(item);
417 if (!fLocs)
418 return;
419 const auto regions = fLocs->info().regions;
420 // Handle ids here
421 if (!qmlObject->idStr().isEmpty()) {
422 addHighlight(regions[IdTokenRegion], QmlHighlightKind::QmlProperty);
423 addHighlight(regions[IdNameRegion], QmlHighlightKind::QmlLocalId);
424 }
425 // If dotted name, then defer it to be handled in ScriptIdentifierExpression
426 if (qmlObject->name().contains("."_L1))
427 return;
428
429 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlType);
430}
431
432void HighlightingVisitor::highlightComponent(const DomItem &item)
433{
434 const auto fLocs = FileLocations::treeOf(item);
435 if (!fLocs)
436 return;
437 const auto regions = fLocs->info().regions;
438 const auto componentKeywordIt = regions.constFind(ComponentKeywordRegion);
439 if (componentKeywordIt == regions.constEnd())
440 return; // not an inline component, no need for highlighting
441 addHighlight(*componentKeywordIt, QmlHighlightKind::QmlKeyword);
442 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlType);
443}
444
445void HighlightingVisitor::highlightPropertyDefinition(const DomItem &item)
446{
447 const auto propertyDef = item.as<PropertyDefinition>();
448 Q_ASSERT(propertyDef);
449 const auto fLocs = FileLocations::treeOf(item);
450 if (!fLocs)
451 return;
452 const auto regions = fLocs->info().regions;
453 QmlHighlightModifiers modifier = QmlHighlightModifier::QmlPropertyDefinition;
454 if (propertyDef->isDefaultMember) {
456 addHighlight(regions[DefaultKeywordRegion], QmlHighlightKind::QmlKeyword);
457 }
458 if (propertyDef->isFinal) {
460 addHighlight(regions[FinalKeywordRegion], QmlHighlightKind::QmlKeyword);
461 }
462 if (propertyDef->isRequired) {
464 addHighlight(regions[RequiredKeywordRegion], QmlHighlightKind::QmlKeyword);
465 }
466 if (propertyDef->isReadonly) {
468 addHighlight(regions[ReadonlyKeywordRegion], QmlHighlightKind::QmlKeyword);
469 }
470 addHighlight(regions[PropertyKeywordRegion], QmlHighlightKind::QmlKeyword);
471 if (propertyDef->isAlias())
472 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlKeyword);
473 else
474 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlType);
475
476 addHighlight(regions[TypeModifierRegion], QmlHighlightKind::QmlTypeModifier);
477 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlProperty,
478 modifier);
479}
480
481void HighlightingVisitor::highlightMethod(const DomItem &item)
482{
483 const auto method = item.as<MethodInfo>();
484 Q_ASSERT(method);
485 const auto fLocs = FileLocations::treeOf(item);
486 if (!fLocs)
487 return;
488 const auto regions = fLocs->info().regions;
489 switch (method->methodType) {
490 case MethodInfo::Signal: {
491 addHighlight(regions[SignalKeywordRegion], QmlHighlightKind::QmlKeyword);
492 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlMethod);
493 break;
494 }
495 case MethodInfo::Method: {
496 addHighlight(regions[FunctionKeywordRegion], QmlHighlightKind::QmlKeyword);
497 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlMethod);
498 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlType);
499 break;
500 }
501 default:
502 Q_UNREACHABLE();
503 }
504
505 for (auto i = 0; i < method->parameters.size(); ++i) {
506 DomItem parameter = item.field(Fields::parameters).index(i);
507 const auto paramRegions = FileLocations::treeOf(parameter)->info().regions;
508 addHighlight(paramRegions[IdentifierRegion],
509 QmlHighlightKind::QmlMethodParameter);
510 addHighlight(paramRegions[TypeIdentifierRegion], QmlHighlightKind::QmlType);
511 }
512 return;
513}
514
515void HighlightingVisitor::highlightScriptLiteral(const DomItem &item)
516{
517 const auto literal = item.as<ScriptElements::Literal>();
518 Q_ASSERT(literal);
519 const auto fLocs = FileLocations::treeOf(item);
520 if (!fLocs)
521 return;
522 const auto regions = fLocs->info().regions;
523 if (std::holds_alternative<QString>(literal->literalValue())) {
524 const auto file = item.containingFile().as<QmlFile>();
525 if (!file)
526 return;
527 const auto &code = file->engine()->code();
528 const auto offset = regions[MainRegion].offset;
529 const auto length = regions[MainRegion].length;
530 const QStringView literalCode = QStringView{code}.mid(offset, length);
531 const auto &locs = Utils::sourceLocationsFromMultiLineToken(
532 literalCode, regions[MainRegion]);
533 for (const auto &loc : locs)
534 addHighlight(loc, QmlHighlightKind::String);
535 } else if (std::holds_alternative<double>(literal->literalValue()))
536 addHighlight(regions[MainRegion], QmlHighlightKind::Number);
537 else if (std::holds_alternative<bool>(literal->literalValue()))
538 addHighlight(regions[MainRegion], QmlHighlightKind::QmlKeyword);
539 else if (std::holds_alternative<std::nullptr_t>(literal->literalValue()))
540 addHighlight(regions[MainRegion], QmlHighlightKind::QmlKeyword);
541 else
542 qCWarning(semanticTokens) << "Invalid literal variant";
543}
544
545void HighlightingVisitor::highlightIdentifier(const DomItem &item)
546{
547 using namespace QLspSpecification;
548 const auto id = item.as<ScriptElements::IdentifierExpression>();
549 Q_ASSERT(id);
550 const auto loc = id->mainRegionLocation();
551 // Many of the scriptIdentifiers expressions are already handled by
552 // other cases. In those cases, if the location offset is already in the list
553 // we don't need to perform expensive resolveExpressionType operation.
554 if (m_highlights.contains(loc.offset))
555 return;
556
557 // If the item is a field member base, we need to resolve the expression type
558 // If the item is a field member access, we don't need to resolve the expression type
559 // because it is already resolved in the first element.
560 if (QQmlLSUtils::isFieldMemberAccess(item))
561 highlightFieldMemberAccess(item, loc);
562 else
563 highlightBySemanticAnalysis(item, loc);
564}
565
566void HighlightingVisitor::highlightCallExpression(const DomItem &item)
567{
568 const auto highlight = [this](const DomItem &item) {
569 if (item.internalKind() == DomType::ScriptIdentifierExpression) {
570 const auto id = item.as<ScriptElements::IdentifierExpression>();
571 Q_ASSERT(id);
572 const auto loc = id->mainRegionLocation();
573 addHighlight(loc, QmlHighlightKind::QmlMethod);
574 }
575 };
576
577 if (item.internalKind() == DomType::ScriptCallExpression) {
578 // If the item is a call expression, we need to highlight the callee.
579 const auto callee = item.field(Fields::callee);
580 if (callee.internalKind() == DomType::ScriptIdentifierExpression) {
581 highlight(callee);
582 return;
583 } else if (callee.internalKind() == DomType::ScriptBinaryExpression) {
584 // If the callee is a binary expression, we need to highlight the right part.
585 const auto right = callee.field(Fields::right);
586 if (right.internalKind() == DomType::ScriptIdentifierExpression)
587 highlight(right);
588 return;
589 }
590 }
591}
592
593void HighlightingVisitor::highlightFieldMemberAccess(const DomItem &item,
594 QQmlJS::SourceLocation loc)
595{
596 // enum fields and qualified module identifiers are not just fields. Do semantic analysis if
597 // the identifier name is an uppercase string.
598 const auto name = item.field(Fields::identifier).value().toString();
599 if (!name.isEmpty() && name.at(0).category() == QChar::Letter_Uppercase) {
600 // maybe the identifier is an attached type or enum members, use semantic analysis to figure
601 // out.
602 return highlightBySemanticAnalysis(item, loc);
603 }
604 // Check if the name is a method
605 const auto expression =
606 QQmlLSUtils::resolveExpressionType(item, QQmlLSUtils::ResolveOptions::ResolveOwnerType);
607
608 if (!expression) {
609 addHighlight(loc, QmlHighlightKind::Field);
610 return;
611 }
612
613 if (expression->type == QQmlLSUtils::MethodIdentifier
614 || expression->type == QQmlLSUtils::LambdaMethodIdentifier) {
615 addHighlight(loc, QmlHighlightKind::QmlMethod);
616 return;
617 } else {
618 return addHighlight(loc, QmlHighlightKind::Field);
619 }
620}
621
622void HighlightingVisitor::highlightBySemanticAnalysis(const DomItem &item, QQmlJS::SourceLocation loc)
623{
624 const auto expression = QQmlLSUtils::resolveExpressionType(
625 item, QQmlLSUtils::ResolveOptions::ResolveOwnerType);
626
627 if (!expression) {
628 addHighlight(loc, QmlHighlightKind::Unknown);
629 return;
630 }
631 switch (expression->type) {
632 case QQmlLSUtils::QmlComponentIdentifier:
633 addHighlight(loc, QmlHighlightKind::QmlType);
634 return;
635 case QQmlLSUtils::JavaScriptIdentifier: {
637 QmlHighlightModifiers modifier = QmlHighlightModifier::None;
638 if (const auto scope = expression->semanticScope) {
639 if (const auto jsIdentifier = scope->jsIdentifier(*expression->name)) {
640 if (jsIdentifier->kind == QQmlJSScope::JavaScriptIdentifier::Parameter)
642 if (jsIdentifier->isConst) {
644 }
645 addHighlight(loc, tokenType, modifier);
646 return;
647 }
648 }
649 if (const auto name = expression->name) {
650 if (const auto highlightKind = resolveJsGlobalObjectKind(item, *name))
651 return addHighlight(loc, *highlightKind);
652 }
653 return;
654 }
655 case QQmlLSUtils::PropertyIdentifier: {
656 if (const auto scope = expression->semanticScope) {
658 if (scope == item.qmlObject().semanticScope()) {
660 } else if (scope == item.rootQmlObject(GoTo::MostLikely).semanticScope()) {
662 } else {
664 }
665 const auto property = scope->property(expression->name.value());
666 QmlHighlightModifiers modifier = QmlHighlightModifier::None;
667 if (!property.isWritable())
669 addHighlight(loc, tokenType, modifier);
670 }
671 return;
672 }
673 case QQmlLSUtils::PropertyChangedSignalIdentifier:
674 addHighlight(loc, QmlHighlightKind::QmlSignal);
675 return;
676 case QQmlLSUtils::PropertyChangedHandlerIdentifier:
677 addHighlight(loc, QmlHighlightKind::QmlSignalHandler);
678 return;
679 case QQmlLSUtils::SignalIdentifier:
680 addHighlight(loc, QmlHighlightKind::QmlSignal);
681 return;
682 case QQmlLSUtils::SignalHandlerIdentifier:
683 addHighlight(loc, QmlHighlightKind::QmlSignalHandler);
684 return;
685 case QQmlLSUtils::MethodIdentifier:
686 addHighlight(loc, QmlHighlightKind::QmlMethod);
687 return;
688 case QQmlLSUtils::QmlObjectIdIdentifier: {
689 const auto qmlfile = item.fileObject().as<QmlFile>();
690 if (!qmlfile) {
691 addHighlight(loc, QmlHighlightKind::Unknown);
692 return;
693 }
694 const auto resolver = qmlfile->typeResolver();
695 if (!resolver) {
696 addHighlight(loc, QmlHighlightKind::Unknown);
697 return;
698 }
699 const auto &objects = resolver->objectsById();
700 if (expression->name.has_value()) {
701 const auto &name = expression->name.value();
702 const auto boundName =
703 objects.id(expression->semanticScope, item.qmlObject().semanticScope());
704 if (!boundName.isEmpty() && name == boundName) {
705 // If the name is the same as the bound name, then it is a local id.
706 addHighlight(loc, QmlHighlightKind::QmlLocalId);
707 return;
708 } else {
709 addHighlight(loc, QmlHighlightKind::QmlExternalId);
710 return;
711 }
712 } else {
713 addHighlight(loc, QmlHighlightKind::QmlExternalId);
714 return;
715 }
716 }
717 case QQmlLSUtils::SingletonIdentifier:
718 addHighlight(loc, QmlHighlightKind::QmlType);
719 return;
720 case QQmlLSUtils::EnumeratorIdentifier:
721 addHighlight(loc, QmlHighlightKind::QmlEnumName);
722 return;
723 case QQmlLSUtils::EnumeratorValueIdentifier:
724 addHighlight(loc, QmlHighlightKind::QmlEnumMember);
725 return;
726 case QQmlLSUtils::AttachedTypeIdentifier:
727 case QQmlLSUtils::AttachedTypeIdentifierInBindingTarget:
728 addHighlight(loc, QmlHighlightKind::QmlType);
729 return;
730 case QQmlLSUtils::GroupedPropertyIdentifier:
731 addHighlight(loc, QmlHighlightKind::QmlProperty);
732 return;
733 case QQmlLSUtils::QualifiedModuleIdentifier:
734 addHighlight(loc, QmlHighlightKind::QmlNamespace);
735 return;
736 default:
737 qCWarning(semanticTokens)
738 << QString::fromLatin1("Semantic token for %1 has not been implemented yet")
739 .arg(int(expression->type));
740 }
741}
742
743void HighlightingVisitor::highlightScriptExpressions(const DomItem &item)
744{
745 const auto fLocs = FileLocations::treeOf(item);
746 if (!fLocs)
747 return;
748 const auto regions = fLocs->info().regions;
749 switch (item.internalKind()) {
750 case DomType::ScriptLiteral:
751 highlightScriptLiteral(item);
752 return;
753 case DomType::ScriptForStatement:
754 addHighlight(regions[ForKeywordRegion], QmlHighlightKind::QmlKeyword);
755 addHighlight(regions[TypeIdentifierRegion],
756 QmlHighlightKind::QmlKeyword);
757 return;
758
759 case DomType::ScriptVariableDeclaration: {
760 addHighlight(regions[TypeIdentifierRegion],
761 QmlHighlightKind::QmlKeyword);
762 return;
763 }
764 case DomType::ScriptReturnStatement:
765 addHighlight(regions[ReturnKeywordRegion], QmlHighlightKind::QmlKeyword);
766 return;
767 case DomType::ScriptCaseClause:
768 addHighlight(regions[CaseKeywordRegion], QmlHighlightKind::QmlKeyword);
769 return;
770 case DomType::ScriptDefaultClause:
771 addHighlight(regions[DefaultKeywordRegion], QmlHighlightKind::QmlKeyword);
772 return;
773 case DomType::ScriptSwitchStatement:
774 addHighlight(regions[SwitchKeywordRegion], QmlHighlightKind::QmlKeyword);
775 return;
776 case DomType::ScriptWhileStatement:
777 addHighlight(regions[WhileKeywordRegion], QmlHighlightKind::QmlKeyword);
778 return;
779 case DomType::ScriptDoWhileStatement:
780 addHighlight(regions[DoKeywordRegion], QmlHighlightKind::QmlKeyword);
781 addHighlight(regions[WhileKeywordRegion], QmlHighlightKind::QmlKeyword);
782 return;
783 case DomType::ScriptTryCatchStatement:
784 addHighlight(regions[TryKeywordRegion], QmlHighlightKind::QmlKeyword);
785 addHighlight(regions[CatchKeywordRegion], QmlHighlightKind::QmlKeyword);
786 addHighlight(regions[FinallyKeywordRegion], QmlHighlightKind::QmlKeyword);
787 return;
788 case DomType::ScriptForEachStatement:
789 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlKeyword);
790 addHighlight(regions[ForKeywordRegion], QmlHighlightKind::QmlKeyword);
791 addHighlight(regions[InOfTokenRegion], QmlHighlightKind::QmlKeyword);
792 return;
793 case DomType::ScriptThrowStatement:
794 addHighlight(regions[ThrowKeywordRegion], QmlHighlightKind::QmlKeyword);
795 return;
796 case DomType::ScriptBreakStatement:
797 addHighlight(regions[BreakKeywordRegion], QmlHighlightKind::QmlKeyword);
798 return;
799 case DomType::ScriptContinueStatement:
800 addHighlight(regions[ContinueKeywordRegion], QmlHighlightKind::QmlKeyword);
801 return;
802 case DomType::ScriptIfStatement:
803 addHighlight(regions[IfKeywordRegion], QmlHighlightKind::QmlKeyword);
804 addHighlight(regions[ElseKeywordRegion], QmlHighlightKind::QmlKeyword);
805 return;
806 case DomType::ScriptLabelledStatement:
807 addHighlight(regions[IdentifierRegion], QmlHighlightKind::JsLabel);
808 return;
809 case DomType::ScriptConditionalExpression:
810 addHighlight(regions[QuestionMarkTokenRegion], QmlHighlightKind::Operator);
811 addHighlight(regions[ColonTokenRegion], QmlHighlightKind::Operator);
812 return;
813 case DomType::ScriptUnaryExpression:
814 case DomType::ScriptPostExpression:
815 addHighlight(regions[OperatorTokenRegion], QmlHighlightKind::Operator);
816 return;
817 case DomType::ScriptType:
818 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlType);
819 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlType);
820 return;
821 case DomType::ScriptFunctionExpression: {
822 addHighlight(regions[FunctionKeywordRegion], QmlHighlightKind::QmlKeyword);
823 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlMethod);
824 return;
825 }
826 case DomType::ScriptYieldExpression:
827 addHighlight(regions[YieldKeywordRegion], QmlHighlightKind::QmlKeyword);
828 return;
829 case DomType::ScriptThisExpression:
830 addHighlight(regions[ThisKeywordRegion], QmlHighlightKind::QmlKeyword);
831 return;
832 case DomType::ScriptSuperLiteral:
833 addHighlight(regions[SuperKeywordRegion], QmlHighlightKind::QmlKeyword);
834 return;
835 case DomType::ScriptNewMemberExpression:
836 case DomType::ScriptNewExpression:
837 addHighlight(regions[NewKeywordRegion], QmlHighlightKind::QmlKeyword);
838 return;
839 case DomType::ScriptTemplateExpressionPart:
840 addHighlight(regions[DollarLeftBraceTokenRegion], QmlHighlightKind::Operator);
841 visitor(Path(), item.field(Fields::expression), false);
842 addHighlight(regions[RightBraceRegion], QmlHighlightKind::Operator);
843 return;
844 case DomType::ScriptTemplateLiteral:
845 addHighlight(regions[LeftBacktickTokenRegion], QmlHighlightKind::String);
846 addHighlight(regions[RightBacktickTokenRegion], QmlHighlightKind::String);
847 return;
848 case DomType::ScriptTemplateStringPart: {
849 // handle multiline case
850 QString code = item.field(Fields::value).value().toString();
851 const auto &locs = Utils::sourceLocationsFromMultiLineToken(
852 code, regions[MainRegion]);
853 for (const auto &loc : locs)
854 addHighlight(loc, QmlHighlightKind::String);
855 return;
856 }
857 default:
858 qCDebug(semanticTokens) << "Script Expressions with kind" << item.internalKind()
859 << "not implemented";
860 }
861}
862
863void HighlightingVisitor::addHighlight(const QQmlJS::SourceLocation &loc, QmlHighlightKind highlightKind,
864 QmlHighlightModifiers modifierKind)
865{
866 return Utils::addHighlight(m_highlights, loc, highlightKind, modifierKind);
867}
868
869/*!
870\internal
871\brief Returns multiple source locations for a given raw comment
872
873Needed by semantic highlighting of comments. LSP clients usually don't support multiline
874tokens. In QML, we can have multiline tokens like string literals and comments.
875This method generates multiple source locations of sub-elements of token split by a newline
876delimiter.
877*/
879Utils::sourceLocationsFromMultiLineToken(QStringView stringLiteral,
880 const QQmlJS::SourceLocation &locationInDocument)
881{
882 auto lineBreakLength = qsizetype(std::char_traits<char>::length("\n"));
883 const auto lineLengths = [&lineBreakLength](QStringView literal) {
884 std::vector<qsizetype> lineLengths;
885 qsizetype startIndex = 0;
886 qsizetype pos = literal.indexOf(u'\n');
887 while (pos != -1) {
888 // TODO: QTBUG-106813
889 // Since a document could be opened in normalized form
890 // we can't use platform dependent newline handling here.
891 // Thus, we check manually if the literal contains \r so that we split
892 // the literal at the correct offset.
893 if (pos - 1 > 0 && literal[pos - 1] == u'\r') {
894 // Handle Windows line endings
895 lineBreakLength = qsizetype(std::char_traits<char>::length("\r\n"));
896 // Move pos to the index of '\r'
897 pos = pos - 1;
898 }
899 lineLengths.push_back(pos - startIndex);
900 // Advance the lookup index, so it won't find the same index.
901 startIndex = pos + lineBreakLength;
902 pos = literal.indexOf('\n'_L1, startIndex);
903 }
904 // Push the last line
905 if (startIndex < literal.length()) {
906 lineLengths.push_back(literal.length() - startIndex);
907 }
908 return lineLengths;
909 };
910
911 QList<QQmlJS::SourceLocation> result;
912 // First token location should start from the "stringLiteral"'s
913 // location in the qml document.
914 QQmlJS::SourceLocation lineLoc = locationInDocument;
915 for (const auto lineLength : lineLengths(stringLiteral)) {
916 lineLoc.length = lineLength;
917 result.push_back(lineLoc);
918
919 // update for the next line
920 lineLoc.offset += lineLoc.length + lineBreakLength;
921 ++lineLoc.startLine;
922 lineLoc.startColumn = 1;
923 }
924 return result;
925}
926
927QList<int> Utils::encodeSemanticTokens(const HighlightsContainer &highlights, HighlightingMode mode)
928{
929 QList<int> result;
930 constexpr auto tokenEncodingLength = 5;
931 result.reserve(tokenEncodingLength * highlights.size());
932
933 int prevLine = 0;
934 int prevColumn = 0;
935 const auto m_mapToProtocol = mode == HighlightingMode::Default
936 ? mapToProtocolDefault
937 : mapToProtocolForQtCreator;
938 std::for_each(highlights.constBegin(), highlights.constEnd(), [&](const auto &token) {
939 int length = token.loc.length;
940 int line = token.loc.startLine - 1; // protocol is 0-based
941 int col = token.loc.startColumn - 1; // protocol is 0-based
942 Q_ASSERT(line >= prevLine);
943 if (line != prevLine)
944 prevColumn = 0;
945 result.emplace_back(line - prevLine);
946 result.emplace_back(col - prevColumn);
947 result.emplace_back(length);
948 result.emplace_back(m_mapToProtocol(token.kind));
949 result.emplace_back(fromQmlModifierKindToLspTokenType(token.modifiers));
950 prevLine = line;
951 prevColumn = col;
952 });
953
954 return result;
955}
956
957/*!
958\internal
959Computes the modifier value. Modifier is read as binary value in the protocol. The location
960of the bits set are interpreted as the indices of the tokenModifiers list registered by the
961server. Then, the client modifies the highlighting of the token.
962
963tokenModifiersList: ["declaration", definition, readonly, static ,,,]
964
965To set "definition" and "readonly", we need to send 0b00000110
966*/
967void Utils::addModifier(SemanticTokenModifiers modifier, int *baseModifier)
968{
969 if (!baseModifier)
970 return;
971 *baseModifier |= (1 << int(modifier));
972}
973
974/*!
975\internal
976Check if the ranges overlap by ensuring that one range starts before the other ends
977*/
978bool Utils::rangeOverlapsWithSourceLocation(const QQmlJS::SourceLocation &loc,
979 const HighlightsRange &r)
980{
981 int startOffsetItem = int(loc.offset);
982 int endOffsetItem = startOffsetItem + int(loc.length);
983 return (startOffsetItem <= r.endOffset) && (r.startOffset <= endOffsetItem);
984}
985
986/*
987\internal
988Increments the resultID by one.
989*/
990void Utils::updateResultID(QByteArray &resultID)
991{
992 int length = resultID.length();
993 for (int i = length - 1; i >= 0; --i) {
994 if (resultID[i] == '9') {
995 resultID[i] = '0';
996 } else {
997 resultID[i] = resultID[i] + 1;
998 return;
999 }
1000 }
1001 resultID.prepend('1');
1002}
1003
1004/*
1005\internal
1006A utility method that computes the difference of two list. The first argument is the encoded token data
1007of the file before edited. The second argument is the encoded token data after the file is edited. Returns
1008a list of SemanticTokensEdit as expected by the protocol.
1009*/
1010QList<SemanticTokensEdit> Utils::computeDiff(const QList<int> &oldData, const QList<int> &newData)
1011{
1012 // Find the iterators pointing the first mismatch, from the start
1013 const auto [oldStart, newStart] =
1014 std::mismatch(oldData.cbegin(), oldData.cend(), newData.cbegin(), newData.cend());
1015
1016 // Find the iterators pointing the first mismatch, from the end
1017 // but the iterators shouldn't pass over the start iterators found above.
1018 const auto [r1, r2] = std::mismatch(oldData.crbegin(), std::make_reverse_iterator(oldStart),
1019 newData.crbegin(), std::make_reverse_iterator(newStart));
1020 const auto oldEnd = r1.base();
1021 const auto newEnd = r2.base();
1022
1023 // no change
1024 if (oldStart == oldEnd && newStart == newEnd)
1025 return {};
1026
1027 SemanticTokensEdit edit;
1028 edit.start = int(std::distance(newData.cbegin(), newStart));
1029 edit.deleteCount = int(std::distance(oldStart, oldEnd));
1030
1031 if (newStart >= newData.cbegin() && newEnd <= newData.cend() && newStart < newEnd)
1032 edit.data.emplace(newStart, newEnd);
1033
1034 return { std::move(edit) };
1035}
1036
1037void Utils::addHighlight(HighlightsContainer &out,
1038 const QQmlJS::SourceLocation &loc,
1039 QmlHighlightKind highlightKind,
1040 QmlHighlightModifiers modifierKind)
1041{
1042 if (!loc.isValid() || loc.length == 0) {
1043 qCDebug(semanticTokens)
1044 << "Invalid locations: Cannot add highlight to token";
1045 return;
1046 }
1047 if (!out.contains(loc.offset))
1048 out.insert(loc.offset, HighlightToken(loc, highlightKind, modifierKind));
1049}
1050
1051HighlightsContainer Utils::visitTokens(const QQmlJS::Dom::DomItem &item,
1052 const std::optional<HighlightsRange> &range)
1053{
1054 using namespace QQmlJS::Dom;
1055 HighlightingVisitor highlightDomElements(item, range);
1056 return highlightDomElements.highlights();
1057}
1058
1059QList<int> Utils::collectTokens(const QQmlJS::Dom::DomItem &item,
1060 const std::optional<HighlightsRange> &range,
1061 HighlightingMode mode)
1062{
1063 return Utils::encodeSemanticTokens(visitTokens(item, range), mode);
1064}
1065
1066} // namespace QmlHighlighting
1067
1068QT_END_NAMESPACE
HighlightingVisitor(const QQmlJS::Dom::DomItem &item, const std::optional< HighlightsRange > &range)
Combined button and popup list for selecting options.
QList< int > encodeSemanticTokens(const HighlightsContainer &highlights, HighlightingMode mode=HighlightingMode::Default)
QList< QLspSpecification::SemanticTokensEdit > computeDiff(const QList< int > &, const QList< int > &)
HighlightsContainer visitTokens(const QQmlJS::Dom::DomItem &item, const std::optional< HighlightsRange > &range)
void addHighlight(HighlightsContainer &out, const QQmlJS::SourceLocation &loc, QmlHighlightKind, QmlHighlightModifiers=QmlHighlightModifier::None)
void updateResultID(QByteArray &resultID)
QList< QQmlJS::SourceLocation > sourceLocationsFromMultiLineToken(QStringView code, const QQmlJS::SourceLocation &tokenLocation)
Returns multiple source locations for a given raw comment.
void addModifier(QLspSpecification::SemanticTokenModifiers modifier, int *baseModifier)
bool rangeOverlapsWithSourceLocation(const QQmlJS::SourceLocation &loc, const HighlightsRange &r)
QList< int > collectTokens(const QQmlJS::Dom::DomItem &item, const std::optional< HighlightsRange > &range, HighlightingMode mode=HighlightingMode::Default)
static FieldFilter highlightingFilter()
static std::optional< QmlHighlightKind > resolveJsGlobalObjectKind(const DomItem &item, const QString &name)
Further resolves the type of a JavaScriptIdentifier A global object can be in the object form or in t...
static int mapToProtocolForQtCreator(QmlHighlightKind highlightKind)
static int fromQmlModifierKindToLspTokenType(QmlHighlightModifiers highlightModifier)
static int mapToProtocolDefault(QmlHighlightKind highlightKind)
HighlightToken(const QQmlJS::SourceLocation &loc, QmlHighlightKind, QmlHighlightModifiers=QmlHighlightModifier::None)