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#include <qqmldiffer_p.h>
7
8#include <QtQmlLS/private/qqmllsutils_p.h>
9#include <QtQmlDom/private/qqmldomscriptelements_p.h>
10#include <QtQmlDom/private/qqmldomfieldfilter_p.h>
11
12#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
13
15
16Q_LOGGING_CATEGORY(semanticTokens, "qt.languageserver.semanticTokens")
17
18using namespace QQmlJS::AST;
19using namespace QQmlJS::Dom;
20using namespace QLspSpecification;
21
22namespace QmlHighlighting {
23
25{
26 switch (highlightKind) {
84 default:
86 }
87}
88
89static int mapToProtocolDefault(QmlHighlightKind highlightKind)
90{
91 switch (highlightKind) {
145 default:
147 }
148}
149
150/*!
151\internal
152\brief Further resolves the type of a JavaScriptIdentifier
153A global object can be in the object form or in the function form.
154For example, Date can be used as a constructor function (like new Date())
155or as a object (like Date.now()).
156*/
158 const QString &name)
159{
160 // Some objects are not constructable, they are always objects.
161 static QSet<QString> noConstructorObjects = { u"Math"_s, u"JSON"_s, u"Atomics"_s, u"Reflect"_s,
162 u"console"_s };
163 // if the method name is in the list of noConstructorObjects, then it is a global object. Do not
164 // perform further checks.
165 if (noConstructorObjects.contains(name))
167 // Check if the method is called with new, then it is a constructor function
168 if (item.directParent().internalKind() == DomType::ScriptNewMemberExpression) {
170 }
171 if (DomItem containingCallExpression = item.filterUp(
172 [](DomType k, const DomItem &) { return k == DomType::ScriptCallExpression; },
173 FilterUpOptions::ReturnOuter)) {
174 // Call expression
175 // if callee is binary expression, then the rightest part is the method name
176 const auto callee = containingCallExpression.field(Fields::callee);
177 if (callee.internalKind() == DomType::ScriptBinaryExpression) {
178 const auto right = callee.field(Fields::right);
179 if (right.internalKind() == DomType::ScriptIdentifierExpression
180 && right.field(Fields::identifier).value().toString() == name) {
182 } else {
184 }
185 } else {
187 }
188 }
189 return std::nullopt;
190}
191
192static int fromQmlModifierKindToLspTokenType(QmlHighlightModifiers highlightModifier)
193{
194 using namespace QLspSpecification;
195 using namespace Utils;
196 int modifier = 0;
197
198 if (highlightModifier.testFlag(QmlHighlightModifier::QmlPropertyDefinition))
199 addModifier(SemanticTokenModifiers::Definition, &modifier);
200
201 if (highlightModifier.testFlag(QmlHighlightModifier::QmlDefaultProperty))
202 addModifier(SemanticTokenModifiers::DefaultLibrary, &modifier);
203
204 if (highlightModifier.testFlag(QmlHighlightModifier::QmlVirtualProperty))
205 addModifier(SemanticTokenModifiers::Static, &modifier);
206
207 if (highlightModifier.testFlag(QmlHighlightModifier::QmlOverrideProperty))
208 addModifier(SemanticTokenModifiers::Static, &modifier);
209
210 if (highlightModifier.testFlag(QmlHighlightModifier::QmlFinalProperty))
211 addModifier(SemanticTokenModifiers::Static, &modifier);
212
213 if (highlightModifier.testFlag(QmlHighlightModifier::QmlRequiredProperty))
214 addModifier(SemanticTokenModifiers::Abstract, &modifier);
215
216 if (highlightModifier.testFlag(QmlHighlightModifier::QmlReadonlyProperty))
217 addModifier(SemanticTokenModifiers::Readonly, &modifier);
218
219 return modifier;
220}
221
223{
224 QMultiMap<QString, QString> fieldFilterAdd{};
225 QMultiMap<QString, QString> fieldFilterRemove{
226 { QString(), Fields::propertyInfos.toString() },
227 { QString(), Fields::fileLocationsTree.toString() },
228 { QString(), Fields::importScope.toString() },
229 { QString(), Fields::defaultPropertyName.toString() },
230 { QString(), Fields::get.toString() },
231 };
232 return FieldFilter{ fieldFilterAdd, fieldFilterRemove };
233}
234
235HighlightToken::HighlightToken(const QQmlJS::SourceLocation &loc,
236 QmlHighlightKind kind,
237 QmlHighlightModifiers modifiers)
238 : loc(loc), kind(kind), modifiers(modifiers)
239{
240}
241
242HighlightingVisitor::HighlightingVisitor(const QQmlJS::Dom::DomItem &item,
243 const std::optional<HighlightsRange> &range)
244 : m_range(range)
245{
246 item.visitTree(
247 Path(),
248 [this](const Path &path, const DomItem &item, bool b) {
249 return this->visitor(path, item, b);
250 },
251 VisitOption::Default | VisitOption::NoPath, emptyChildrenVisitor, emptyChildrenVisitor,
252 highlightingFilter());
253}
254
255bool HighlightingVisitor::visitor(Path, const DomItem &item, bool)
256{
257 if (m_range.has_value()) {
258 const auto fLocs = FileLocations::treeOf(item);
259 if (!fLocs)
260 return true;
261 const auto regions = fLocs->info().regions;
262 if (!Utils::rangeOverlapsWithSourceLocation(regions[MainRegion],
263 m_range.value()))
264 return true;
265 }
266 switch (item.internalKind()) {
267 case DomType::Comment: {
268 highlightComment(item);
269 return true;
270 }
271 case DomType::Import: {
272 highlightImport(item);
273 return true;
274 }
275 case DomType::Binding: {
276 highlightBinding(item);
277 return true;
278 }
279 case DomType::Pragma: {
280 highlightPragma(item);
281 return true;
282 }
283 case DomType::EnumDecl: {
284 highlightEnumDecl(item);
285 return true;
286 }
287 case DomType::EnumItem: {
288 highlightEnumItem(item);
289 return true;
290 }
291 case DomType::QmlObject: {
292 highlightQmlObject(item);
293 return true;
294 }
295 case DomType::QmlComponent: {
296 highlightComponent(item);
297 return true;
298 }
299 case DomType::PropertyDefinition: {
300 highlightPropertyDefinition(item);
301 return true;
302 }
303 case DomType::MethodInfo: {
304 highlightMethod(item);
305 return true;
306 }
307 case DomType::ScriptLiteral: {
308 highlightScriptLiteral(item);
309 return true;
310 }
311 case DomType::ScriptCallExpression: {
312 highlightCallExpression(item);
313 return true;
314 }
315 case DomType::ScriptIdentifierExpression: {
316 highlightIdentifier(item);
317 return true;
318 }
319 default:
320 if (item.ownerAs<ScriptExpression>())
321 highlightScriptExpressions(item);
322 return true;
323 }
324 Q_UNREACHABLE_RETURN(false);
325}
326
327void HighlightingVisitor::highlightComment(const DomItem &item)
328{
329 const auto comment = item.as<Comment>();
330 Q_ASSERT(comment);
331 const auto locs = Utils::sourceLocationsFromMultiLineToken(
332 comment->info().comment(), comment->info().sourceLocation());
333 for (const auto &loc : locs)
334 addHighlight(loc, QmlHighlightKind::Comment);
335}
336
337void HighlightingVisitor::highlightImport(const DomItem &item)
338{
339 const auto fLocs = FileLocations::treeOf(item);
340 if (!fLocs)
341 return;
342 const auto regions = fLocs->info().regions;
343 const auto import = item.as<Import>();
344 Q_ASSERT(import);
345 addHighlight(regions[ImportTokenRegion], QmlHighlightKind::QmlKeyword);
346 if (import->uri.isModule())
347 addHighlight(regions[ImportUriRegion], QmlHighlightKind::QmlImportId);
348 else
349 addHighlight(regions[ImportUriRegion], QmlHighlightKind::String);
350 if (regions.contains(VersionRegion))
351 addHighlight(regions[VersionRegion], QmlHighlightKind::Number);
352 if (regions.contains(AsTokenRegion)) {
353 addHighlight(regions[AsTokenRegion], QmlHighlightKind::QmlKeyword);
354 addHighlight(regions[IdNameRegion], QmlHighlightKind::QmlNamespace);
355 }
356}
357
358void HighlightingVisitor::highlightBinding(const DomItem &item)
359{
360 const auto binding = item.as<Binding>();
361 Q_ASSERT(binding);
362 const auto fLocs = FileLocations::treeOf(item);
363 if (!fLocs) {
364 qCDebug(semanticTokens) << "Can't find the locations for" << item.internalKind();
365 return;
366 }
367 const auto regions = fLocs->info().regions;
368 // If dotted name, then defer it to be handled in ScriptIdentifierExpression
369 if (binding->name().contains("."_L1))
370 return;
371
372 if (binding->bindingType() != BindingType::Normal) {
373 addHighlight(regions[OnTokenRegion], QmlHighlightKind::QmlKeyword);
374 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlProperty);
375 return;
376 }
377
378 return addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlProperty);
379}
380
381void HighlightingVisitor::highlightPragma(const DomItem &item)
382{
383 const auto fLocs = FileLocations::treeOf(item);
384 if (!fLocs)
385 return;
386 const auto regions = fLocs->info().regions;
387 addHighlight(regions[PragmaKeywordRegion], QmlHighlightKind::QmlKeyword);
388 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlPragmaName );
389 const auto pragma = item.as<Pragma>();
390 for (auto i = 0; i < pragma->values.size(); ++i) {
391 DomItem value = item.field(Fields::values).index(i);
392 const auto valueRegions = FileLocations::treeOf(value)->info().regions;
393 addHighlight(valueRegions[PragmaValuesRegion], QmlHighlightKind::QmlPragmaValue);
394 }
395 return;
396}
397
398void HighlightingVisitor::highlightEnumDecl(const DomItem &item)
399{
400 const auto fLocs = FileLocations::treeOf(item);
401 if (!fLocs)
402 return;
403 const auto regions = fLocs->info().regions;
404 addHighlight(regions[EnumKeywordRegion], QmlHighlightKind::QmlKeyword);
405 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlEnumName);
406}
407
408void HighlightingVisitor::highlightEnumItem(const DomItem &item)
409{
410 const auto fLocs = FileLocations::treeOf(item);
411 if (!fLocs)
412 return;
413 const auto regions = fLocs->info().regions;
414 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlEnumMember);
415 if (regions.contains(EnumValueRegion))
416 addHighlight(regions[EnumValueRegion], QmlHighlightKind::Number);
417}
418
419void HighlightingVisitor::highlightQmlObject(const DomItem &item)
420{
421 const auto qmlObject = item.as<QmlObject>();
422 Q_ASSERT(qmlObject);
423 const auto fLocs = FileLocations::treeOf(item);
424 if (!fLocs)
425 return;
426 const auto regions = fLocs->info().regions;
427 // Handle ids here
428 if (!qmlObject->idStr().isEmpty()) {
429 addHighlight(regions[IdTokenRegion], QmlHighlightKind::QmlProperty);
430 addHighlight(regions[IdNameRegion], QmlHighlightKind::QmlLocalId);
431 }
432 // If dotted name, then defer it to be handled in ScriptIdentifierExpression
433 if (qmlObject->name().contains("."_L1))
434 return;
435
436 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlType);
437}
438
439void HighlightingVisitor::highlightComponent(const DomItem &item)
440{
441 const auto fLocs = FileLocations::treeOf(item);
442 if (!fLocs)
443 return;
444 const auto regions = fLocs->info().regions;
445 const auto componentKeywordIt = regions.constFind(ComponentKeywordRegion);
446 if (componentKeywordIt == regions.constEnd())
447 return; // not an inline component, no need for highlighting
448 addHighlight(*componentKeywordIt, QmlHighlightKind::QmlKeyword);
449 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlType);
450}
451
452void HighlightingVisitor::highlightPropertyDefinition(const DomItem &item)
453{
454 const auto propertyDef = item.as<PropertyDefinition>();
455 Q_ASSERT(propertyDef);
456 const auto fLocs = FileLocations::treeOf(item);
457 if (!fLocs)
458 return;
459 const auto regions = fLocs->info().regions;
460 QmlHighlightModifiers modifier = QmlHighlightModifier::QmlPropertyDefinition;
461 if (propertyDef->isDefaultMember) {
463 addHighlight(regions[DefaultKeywordRegion], QmlHighlightKind::QmlKeyword);
464 }
465 if (propertyDef->isVirtual) {
467 addHighlight(regions[VirtualKeywordRegion], QmlHighlightKind::QmlKeyword);
468 }
469 if (propertyDef->isOverride) {
471 addHighlight(regions[OverrideKeywordRegion], QmlHighlightKind::QmlKeyword);
472 }
473 if (propertyDef->isFinal) {
475 addHighlight(regions[FinalKeywordRegion], QmlHighlightKind::QmlKeyword);
476 }
477 if (propertyDef->isRequired) {
479 addHighlight(regions[RequiredKeywordRegion], QmlHighlightKind::QmlKeyword);
480 }
481 if (propertyDef->isReadonly) {
483 addHighlight(regions[ReadonlyKeywordRegion], QmlHighlightKind::QmlKeyword);
484 }
485 addHighlight(regions[PropertyKeywordRegion], QmlHighlightKind::QmlKeyword);
486 if (propertyDef->isAlias())
487 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlKeyword);
488 else
489 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlType);
490
491 addHighlight(regions[TypeModifierRegion], QmlHighlightKind::QmlTypeModifier);
492 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlProperty,
493 modifier);
494}
495
496void HighlightingVisitor::highlightMethod(const DomItem &item)
497{
498 const auto method = item.as<MethodInfo>();
499 Q_ASSERT(method);
500 const auto fLocs = FileLocations::treeOf(item);
501 if (!fLocs)
502 return;
503 const auto regions = fLocs->info().regions;
504 switch (method->methodType) {
505 case MethodInfo::Signal: {
506 addHighlight(regions[SignalKeywordRegion], QmlHighlightKind::QmlKeyword);
507 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlMethod);
508 break;
509 }
510 case MethodInfo::Method: {
511 addHighlight(regions[FunctionKeywordRegion], QmlHighlightKind::QmlKeyword);
512 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlMethod);
513 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlType);
514 break;
515 }
516 default:
517 Q_UNREACHABLE();
518 }
519
520 for (auto i = 0; i < method->parameters.size(); ++i) {
521 DomItem parameter = item.field(Fields::parameters).index(i);
522 const auto paramRegions = FileLocations::treeOf(parameter)->info().regions;
523 addHighlight(paramRegions[IdentifierRegion],
524 QmlHighlightKind::QmlMethodParameter);
525 addHighlight(paramRegions[TypeIdentifierRegion], QmlHighlightKind::QmlType);
526 }
527 return;
528}
529
530void HighlightingVisitor::highlightScriptLiteral(const DomItem &item)
531{
532 const auto literal = item.as<ScriptElements::Literal>();
533 Q_ASSERT(literal);
534 const auto fLocs = FileLocations::treeOf(item);
535 if (!fLocs)
536 return;
537 const auto regions = fLocs->info().regions;
538 if (std::holds_alternative<QString>(literal->literalValue())) {
539 const auto file = item.containingFile().as<QmlFile>();
540 if (!file)
541 return;
542 const auto &code = file->engine()->code();
543 const auto offset = regions[MainRegion].offset;
544 const auto length = regions[MainRegion].length;
545 const QStringView literalCode = QStringView{code}.mid(offset, length);
546 const auto &locs = Utils::sourceLocationsFromMultiLineToken(
547 literalCode, regions[MainRegion]);
548 for (const auto &loc : locs)
549 addHighlight(loc, QmlHighlightKind::String);
550 } else if (std::holds_alternative<double>(literal->literalValue()))
551 addHighlight(regions[MainRegion], QmlHighlightKind::Number);
552 else if (std::holds_alternative<bool>(literal->literalValue()))
553 addHighlight(regions[MainRegion], QmlHighlightKind::QmlKeyword);
554 else if (std::holds_alternative<std::nullptr_t>(literal->literalValue()))
555 addHighlight(regions[MainRegion], QmlHighlightKind::QmlKeyword);
556 else
557 qCWarning(semanticTokens) << "Invalid literal variant";
558}
559
560void HighlightingVisitor::highlightIdentifier(const DomItem &item)
561{
562 using namespace QLspSpecification;
563 const auto id = item.as<ScriptElements::IdentifierExpression>();
564 Q_ASSERT(id);
565 const auto loc = id->mainRegionLocation();
566 // Many of the scriptIdentifiers expressions are already handled by
567 // other cases. In those cases, if the location offset is already in the list
568 // we don't need to perform expensive resolveExpressionType operation.
569 if (m_highlights.contains(loc.offset))
570 return;
571
572 // If the item is a field member base, we need to resolve the expression type
573 // If the item is a field member access, we don't need to resolve the expression type
574 // because it is already resolved in the first element.
575 if (QQmlLSUtils::isFieldMemberAccess(item))
576 highlightFieldMemberAccess(item, loc);
577 else
578 highlightBySemanticAnalysis(item, loc);
579}
580
581void HighlightingVisitor::highlightCallExpression(const DomItem &item)
582{
583 const auto highlight = [this](const DomItem &item) {
584 if (item.internalKind() == DomType::ScriptIdentifierExpression) {
585 const auto id = item.as<ScriptElements::IdentifierExpression>();
586 Q_ASSERT(id);
587 const auto loc = id->mainRegionLocation();
588 addHighlight(loc, QmlHighlightKind::QmlMethod);
589 }
590 };
591
592 if (item.internalKind() == DomType::ScriptCallExpression) {
593 // If the item is a call expression, we need to highlight the callee.
594 const auto callee = item.field(Fields::callee);
595 if (callee.internalKind() == DomType::ScriptIdentifierExpression) {
596 highlight(callee);
597 return;
598 } else if (callee.internalKind() == DomType::ScriptBinaryExpression) {
599 // If the callee is a binary expression, we need to highlight the right part.
600 const auto right = callee.field(Fields::right);
601 if (right.internalKind() == DomType::ScriptIdentifierExpression)
602 highlight(right);
603 return;
604 }
605 }
606}
607
608void HighlightingVisitor::highlightFieldMemberAccess(const DomItem &item,
609 QQmlJS::SourceLocation loc)
610{
611 // enum fields and qualified module identifiers are not just fields. Do semantic analysis if
612 // the identifier name is an uppercase string.
613 const auto name = item.field(Fields::identifier).value().toString();
614 if (!name.isEmpty() && name.at(0).category() == QChar::Letter_Uppercase) {
615 // maybe the identifier is an attached type or enum members, use semantic analysis to figure
616 // out.
617 return highlightBySemanticAnalysis(item, loc);
618 }
619 // Check if the name is a method
620 const auto expression =
621 QQmlLSUtils::resolveExpressionType(item, QQmlLSUtils::ResolveOptions::ResolveOwnerType);
622
623 if (!expression) {
624 addHighlight(loc, QmlHighlightKind::Field);
625 return;
626 }
627
628 if (expression->type == QQmlLSUtils::MethodIdentifier
629 || expression->type == QQmlLSUtils::LambdaMethodIdentifier) {
630 addHighlight(loc, QmlHighlightKind::QmlMethod);
631 return;
632 } else {
633 return addHighlight(loc, QmlHighlightKind::Field);
634 }
635}
636
637void HighlightingVisitor::highlightBySemanticAnalysis(const DomItem &item, QQmlJS::SourceLocation loc)
638{
639 const auto expression = QQmlLSUtils::resolveExpressionType(
640 item, QQmlLSUtils::ResolveOptions::ResolveOwnerType);
641
642 if (!expression) {
643 addHighlight(loc, QmlHighlightKind::Unknown);
644 return;
645 }
646 switch (expression->type) {
647 case QQmlLSUtils::QmlComponentIdentifier:
648 addHighlight(loc, QmlHighlightKind::QmlType);
649 return;
650 case QQmlLSUtils::JavaScriptIdentifier: {
652 QmlHighlightModifiers modifier = QmlHighlightModifier::None;
653 if (const auto scope = expression->semanticScope) {
654 if (const auto jsIdentifier = scope->jsIdentifier(*expression->name)) {
655 if (jsIdentifier->kind == QQmlJSScope::JavaScriptIdentifier::Parameter)
657 if (jsIdentifier->isConst) {
659 }
660 addHighlight(loc, tokenType, modifier);
661 return;
662 }
663 }
664 if (const auto name = expression->name) {
665 if (const auto highlightKind = resolveJsGlobalObjectKind(item, *name))
666 return addHighlight(loc, *highlightKind);
667 }
668 return;
669 }
670 case QQmlLSUtils::PropertyIdentifier: {
671 if (const auto scope = expression->semanticScope) {
673 if (scope == item.qmlObject().semanticScope()) {
675 } else if (scope == item.rootQmlObject(GoTo::MostLikely).semanticScope()) {
677 } else {
679 }
680 const auto property = scope->property(expression->name.value());
681 QmlHighlightModifiers modifier = QmlHighlightModifier::None;
682 if (!property.isWritable())
684 addHighlight(loc, tokenType, modifier);
685 }
686 return;
687 }
688 case QQmlLSUtils::PropertyChangedSignalIdentifier:
689 addHighlight(loc, QmlHighlightKind::QmlSignal);
690 return;
691 case QQmlLSUtils::PropertyChangedHandlerIdentifier:
692 addHighlight(loc, QmlHighlightKind::QmlSignalHandler);
693 return;
694 case QQmlLSUtils::SignalIdentifier:
695 addHighlight(loc, QmlHighlightKind::QmlSignal);
696 return;
697 case QQmlLSUtils::SignalHandlerIdentifier:
698 addHighlight(loc, QmlHighlightKind::QmlSignalHandler);
699 return;
700 case QQmlLSUtils::MethodIdentifier:
701 addHighlight(loc, QmlHighlightKind::QmlMethod);
702 return;
703 case QQmlLSUtils::QmlObjectIdIdentifier: {
704 if (!expression->semanticScope) {
705 // In PropertyChanges and friends, this id looks like a generalized grouped property but
706 // is actually custom parsed, so don't highlight it.
707 addHighlight(loc, QmlHighlightKind::Unknown);
708 return;
709 }
710 const auto qmlfile = item.fileObject().as<QmlFile>();
711 if (!qmlfile) {
712 addHighlight(loc, QmlHighlightKind::Unknown);
713 return;
714 }
715 const auto resolver = qmlfile->typeResolver();
716 if (!resolver) {
717 addHighlight(loc, QmlHighlightKind::Unknown);
718 return;
719 }
720 const auto &objects = resolver->objectsById();
721 if (expression->name.has_value()) {
722 const auto &name = expression->name.value();
723 const auto boundName =
724 objects.id(expression->semanticScope, item.qmlObject().semanticScope());
725 if (!boundName.isEmpty() && name == boundName) {
726 // If the name is the same as the bound name, then it is a local id.
727 addHighlight(loc, QmlHighlightKind::QmlLocalId);
728 return;
729 } else {
730 addHighlight(loc, QmlHighlightKind::QmlExternalId);
731 return;
732 }
733 } else {
734 addHighlight(loc, QmlHighlightKind::QmlExternalId);
735 return;
736 }
737 }
738 case QQmlLSUtils::SingletonIdentifier:
739 addHighlight(loc, QmlHighlightKind::QmlType);
740 return;
741 case QQmlLSUtils::EnumeratorIdentifier:
742 addHighlight(loc, QmlHighlightKind::QmlEnumName);
743 return;
744 case QQmlLSUtils::EnumeratorValueIdentifier:
745 addHighlight(loc, QmlHighlightKind::QmlEnumMember);
746 return;
747 case QQmlLSUtils::AttachedTypeIdentifier:
748 case QQmlLSUtils::AttachedTypeIdentifierInBindingTarget:
749 addHighlight(loc, QmlHighlightKind::QmlType);
750 return;
751 case QQmlLSUtils::GroupedPropertyIdentifier:
752 addHighlight(loc, QmlHighlightKind::QmlProperty);
753 return;
754 case QQmlLSUtils::QualifiedModuleIdentifier:
755 addHighlight(loc, QmlHighlightKind::QmlNamespace);
756 return;
757 default:
758 qCWarning(semanticTokens)
759 << QString::fromLatin1("Semantic token for %1 has not been implemented yet")
760 .arg(int(expression->type));
761 }
762}
763
764void HighlightingVisitor::highlightScriptExpressions(const DomItem &item)
765{
766 const auto fLocs = FileLocations::treeOf(item);
767 if (!fLocs)
768 return;
769 const auto regions = fLocs->info().regions;
770 switch (item.internalKind()) {
771 case DomType::ScriptLiteral:
772 highlightScriptLiteral(item);
773 return;
774 case DomType::ScriptForStatement:
775 addHighlight(regions[ForKeywordRegion], QmlHighlightKind::QmlKeyword);
776 addHighlight(regions[TypeIdentifierRegion],
777 QmlHighlightKind::QmlKeyword);
778 return;
779
780 case DomType::ScriptVariableDeclaration: {
781 addHighlight(regions[TypeIdentifierRegion],
782 QmlHighlightKind::QmlKeyword);
783 return;
784 }
785 case DomType::ScriptReturnStatement:
786 addHighlight(regions[ReturnKeywordRegion], QmlHighlightKind::QmlKeyword);
787 return;
788 case DomType::ScriptCaseClause:
789 addHighlight(regions[CaseKeywordRegion], QmlHighlightKind::QmlKeyword);
790 return;
791 case DomType::ScriptDefaultClause:
792 addHighlight(regions[DefaultKeywordRegion], QmlHighlightKind::QmlKeyword);
793 return;
794 case DomType::ScriptSwitchStatement:
795 addHighlight(regions[SwitchKeywordRegion], QmlHighlightKind::QmlKeyword);
796 return;
797 case DomType::ScriptWhileStatement:
798 addHighlight(regions[WhileKeywordRegion], QmlHighlightKind::QmlKeyword);
799 return;
800 case DomType::ScriptDoWhileStatement:
801 addHighlight(regions[DoKeywordRegion], QmlHighlightKind::QmlKeyword);
802 addHighlight(regions[WhileKeywordRegion], QmlHighlightKind::QmlKeyword);
803 return;
804 case DomType::ScriptTryCatchStatement:
805 addHighlight(regions[TryKeywordRegion], QmlHighlightKind::QmlKeyword);
806 addHighlight(regions[CatchKeywordRegion], QmlHighlightKind::QmlKeyword);
807 addHighlight(regions[FinallyKeywordRegion], QmlHighlightKind::QmlKeyword);
808 return;
809 case DomType::ScriptForEachStatement:
810 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlKeyword);
811 addHighlight(regions[ForKeywordRegion], QmlHighlightKind::QmlKeyword);
812 addHighlight(regions[InOfTokenRegion], QmlHighlightKind::QmlKeyword);
813 return;
814 case DomType::ScriptThrowStatement:
815 addHighlight(regions[ThrowKeywordRegion], QmlHighlightKind::QmlKeyword);
816 return;
817 case DomType::ScriptBreakStatement:
818 addHighlight(regions[BreakKeywordRegion], QmlHighlightKind::QmlKeyword);
819 return;
820 case DomType::ScriptContinueStatement:
821 addHighlight(regions[ContinueKeywordRegion], QmlHighlightKind::QmlKeyword);
822 return;
823 case DomType::ScriptIfStatement:
824 addHighlight(regions[IfKeywordRegion], QmlHighlightKind::QmlKeyword);
825 addHighlight(regions[ElseKeywordRegion], QmlHighlightKind::QmlKeyword);
826 return;
827 case DomType::ScriptLabelledStatement:
828 addHighlight(regions[IdentifierRegion], QmlHighlightKind::JsLabel);
829 return;
830 case DomType::ScriptConditionalExpression:
831 addHighlight(regions[QuestionMarkTokenRegion], QmlHighlightKind::Operator);
832 addHighlight(regions[ColonTokenRegion], QmlHighlightKind::Operator);
833 return;
834 case DomType::ScriptUnaryExpression:
835 case DomType::ScriptPostExpression:
836 addHighlight(regions[OperatorTokenRegion], QmlHighlightKind::Operator);
837 return;
838 case DomType::ScriptType:
839 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlType);
840 addHighlight(regions[TypeIdentifierRegion], QmlHighlightKind::QmlType);
841 return;
842 case DomType::ScriptFunctionExpression: {
843 addHighlight(regions[FunctionKeywordRegion], QmlHighlightKind::QmlKeyword);
844 addHighlight(regions[IdentifierRegion], QmlHighlightKind::QmlMethod);
845 return;
846 }
847 case DomType::ScriptYieldExpression:
848 addHighlight(regions[YieldKeywordRegion], QmlHighlightKind::QmlKeyword);
849 return;
850 case DomType::ScriptThisExpression:
851 addHighlight(regions[ThisKeywordRegion], QmlHighlightKind::QmlKeyword);
852 return;
853 case DomType::ScriptSuperLiteral:
854 addHighlight(regions[SuperKeywordRegion], QmlHighlightKind::QmlKeyword);
855 return;
856 case DomType::ScriptNewMemberExpression:
857 case DomType::ScriptNewExpression:
858 addHighlight(regions[NewKeywordRegion], QmlHighlightKind::QmlKeyword);
859 return;
860 case DomType::ScriptTemplateExpressionPart:
861 addHighlight(regions[DollarLeftBraceTokenRegion], QmlHighlightKind::Operator);
862 visitor(Path(), item.field(Fields::expression), false);
863 addHighlight(regions[RightBraceRegion], QmlHighlightKind::Operator);
864 return;
865 case DomType::ScriptTemplateLiteral:
866 addHighlight(regions[LeftBacktickTokenRegion], QmlHighlightKind::String);
867 addHighlight(regions[RightBacktickTokenRegion], QmlHighlightKind::String);
868 return;
869 case DomType::ScriptTemplateStringPart: {
870 // handle multiline case
871 QString code = item.field(Fields::value).value().toString();
872 const auto &locs = Utils::sourceLocationsFromMultiLineToken(
873 code, regions[MainRegion]);
874 for (const auto &loc : locs)
875 addHighlight(loc, QmlHighlightKind::String);
876 return;
877 }
878 default:
879 qCDebug(semanticTokens) << "Script Expressions with kind" << item.internalKind()
880 << "not implemented";
881 }
882}
883
884void HighlightingVisitor::addHighlight(const QQmlJS::SourceLocation &loc, QmlHighlightKind highlightKind,
885 QmlHighlightModifiers modifierKind)
886{
887 return Utils::addHighlight(m_highlights, loc, highlightKind, modifierKind);
888}
889
890/*!
891\internal
892\brief Returns multiple source locations for a given raw comment
893
894Needed by semantic highlighting of comments. LSP clients usually don't support multiline
895tokens. In QML, we can have multiline tokens like string literals and comments.
896This method generates multiple source locations of sub-elements of token split by a newline
897delimiter.
898*/
900Utils::sourceLocationsFromMultiLineToken(QStringView stringLiteral,
901 const QQmlJS::SourceLocation &locationInDocument)
902{
903 auto lineBreakLength = qsizetype(std::char_traits<char>::length("\n"));
904 const auto lineLengths = [&lineBreakLength](QStringView literal) {
905 std::vector<qsizetype> lineLengths;
906 qsizetype startIndex = 0;
907 qsizetype pos = literal.indexOf(u'\n');
908 while (pos != -1) {
909 // TODO: QTBUG-106813
910 // Since a document could be opened in normalized form
911 // we can't use platform dependent newline handling here.
912 // Thus, we check manually if the literal contains \r so that we split
913 // the literal at the correct offset.
914 if (pos - 1 > 0 && literal[pos - 1] == u'\r') {
915 // Handle Windows line endings
916 lineBreakLength = qsizetype(std::char_traits<char>::length("\r\n"));
917 // Move pos to the index of '\r'
918 pos = pos - 1;
919 }
920 lineLengths.push_back(pos - startIndex);
921 // Advance the lookup index, so it won't find the same index.
922 startIndex = pos + lineBreakLength;
923 pos = literal.indexOf('\n'_L1, startIndex);
924 }
925 // Push the last line
926 if (startIndex < literal.length()) {
927 lineLengths.push_back(literal.length() - startIndex);
928 }
929 return lineLengths;
930 };
931
932 QList<QQmlJS::SourceLocation> result;
933 // First token location should start from the "stringLiteral"'s
934 // location in the qml document.
935 QQmlJS::SourceLocation lineLoc = locationInDocument;
936 for (const auto lineLength : lineLengths(stringLiteral)) {
937 lineLoc.length = lineLength;
938 result.push_back(lineLoc);
939
940 // update for the next line
941 lineLoc.offset += lineLoc.length + lineBreakLength;
942 ++lineLoc.startLine;
943 lineLoc.startColumn = 1;
944 }
945 return result;
946}
947
948QList<int> Utils::encodeSemanticTokens(const HighlightsContainer &highlights, HighlightingMode mode)
949{
950 QList<int> result;
951 constexpr auto tokenEncodingLength = 5;
952 result.reserve(tokenEncodingLength * highlights.size());
953
954 int prevLine = 0;
955 int prevColumn = 0;
956 const auto m_mapToProtocol = mode == HighlightingMode::Default
957 ? mapToProtocolDefault
958 : mapToProtocolForQtCreator;
959 std::for_each(highlights.constBegin(), highlights.constEnd(), [&](const auto &token) {
960 int length = token.loc.length;
961 int line = token.loc.startLine - 1; // protocol is 0-based
962 int col = token.loc.startColumn - 1; // protocol is 0-based
963 Q_ASSERT(line >= prevLine);
964 if (line != prevLine)
965 prevColumn = 0;
966 result.emplace_back(line - prevLine);
967 result.emplace_back(col - prevColumn);
968 result.emplace_back(length);
969 result.emplace_back(m_mapToProtocol(token.kind));
970 result.emplace_back(fromQmlModifierKindToLspTokenType(token.modifiers));
971 prevLine = line;
972 prevColumn = col;
973 });
974
975 return result;
976}
977
978/*!
979\internal
980Computes the modifier value. Modifier is read as binary value in the protocol. The location
981of the bits set are interpreted as the indices of the tokenModifiers list registered by the
982server. Then, the client modifies the highlighting of the token.
983
984tokenModifiersList: ["declaration", definition, readonly, static ,,,]
985
986To set "definition" and "readonly", we need to send 0b00000110
987*/
988void Utils::addModifier(SemanticTokenModifiers modifier, int *baseModifier)
989{
990 if (!baseModifier)
991 return;
992 *baseModifier |= (1 << int(modifier));
993}
994
995/*!
996\internal
997Check if the ranges overlap by ensuring that one range starts before the other ends
998*/
999bool Utils::rangeOverlapsWithSourceLocation(const QQmlJS::SourceLocation &loc,
1000 const HighlightsRange &r)
1001{
1002 int startOffsetItem = int(loc.offset);
1003 int endOffsetItem = startOffsetItem + int(loc.length);
1004 return (startOffsetItem <= r.endOffset) && (r.startOffset <= endOffsetItem);
1005}
1006
1007/*
1008\internal
1009Increments the resultID by one.
1010*/
1011void Utils::updateResultID(QByteArray &resultID)
1012{
1013 int length = resultID.length();
1014 for (int i = length - 1; i >= 0; --i) {
1015 if (resultID[i] == '9') {
1016 resultID[i] = '0';
1017 } else {
1018 resultID[i] = resultID[i] + 1;
1019 return;
1020 }
1021 }
1022 resultID.prepend('1');
1023}
1024
1025/*
1026\internal
1027A utility method that computes the difference of two list. The first argument is the encoded token data
1028of the file before edited. The second argument is the encoded token data after the file is edited. Returns
1029a list of SemanticTokensEdit as expected by the protocol.
1030*/
1031QList<SemanticTokensEdit> Utils::computeDiff(const QList<int> &oldData, const QList<int> &newData)
1032{
1033 // Find the iterators pointing the first mismatch, from the start
1034 const auto [oldStart, newStart] =
1035 std::mismatch(oldData.cbegin(), oldData.cend(), newData.cbegin(), newData.cend());
1036
1037 // Find the iterators pointing the first mismatch, from the end
1038 // but the iterators shouldn't pass over the start iterators found above.
1039 const auto [r1, r2] = std::mismatch(oldData.crbegin(), std::make_reverse_iterator(oldStart),
1040 newData.crbegin(), std::make_reverse_iterator(newStart));
1041 const auto oldEnd = r1.base();
1042 const auto newEnd = r2.base();
1043
1044 // no change
1045 if (oldStart == oldEnd && newStart == newEnd)
1046 return {};
1047
1048 SemanticTokensEdit edit;
1049 edit.start = int(std::distance(newData.cbegin(), newStart));
1050 edit.deleteCount = int(std::distance(oldStart, oldEnd));
1051
1052 if (newStart >= newData.cbegin() && newEnd <= newData.cend() && newStart < newEnd)
1053 edit.data.emplace(newStart, newEnd);
1054
1055 return { std::move(edit) };
1056}
1057
1058void Utils::addHighlight(HighlightsContainer &out,
1059 const QQmlJS::SourceLocation &loc,
1060 QmlHighlightKind highlightKind,
1061 QmlHighlightModifiers modifierKind)
1062{
1063 if (!loc.isValid() || loc.length == 0) {
1064 qCDebug(semanticTokens)
1065 << "Invalid locations: Cannot add highlight to token";
1066 return;
1067 }
1068 if (!out.contains(loc.offset))
1069 out.insert(loc.offset, HighlightToken(loc, highlightKind, modifierKind));
1070}
1071
1072HighlightsContainer Utils::visitTokens(const QQmlJS::Dom::DomItem &item,
1073 const std::optional<HighlightsRange> &range)
1074{
1075 using namespace QQmlJS::Dom;
1076 HighlightingVisitor highlightDomElements(item, range);
1077 return highlightDomElements.highlights();
1078}
1079
1080HighlightsContainer Utils::shiftHighlights(const HighlightsContainer &cachedHighlights,
1081 const QString &lastValidCode, const QString &currentCode)
1082{
1083 using namespace QQmlLSUtils;
1084 Differ differ;
1085 const QList<Diff> diffs = differ.diff(lastValidCode, currentCode);
1086 HighlightsContainer shifts = cachedHighlights;
1087 applyDiffs(shifts, diffs);
1088 return shifts;
1089}
1090
1092{
1093 auto [row, col] = QQmlJS::SourceLocation::rowAndColumnFrom(text, text.size());
1094 return { row - 1, col - 1 }; // rows are 1-based, so subtract 1 to get the number of newlines
1095}
1096
1097static void updateCursorPositionByDiff(const QString &text, QQmlJS::SourceLocation &cursor)
1098{
1099 auto [newLines, lastLineLength] = newlineCountAndLastLineLength(text);
1100 if (newLines > 0) {
1101 cursor.startLine += newLines;
1102 cursor.startColumn = lastLineLength + 1; // +1 because columns are 1-based
1103 } else {
1104 cursor.startColumn += text.size();
1105 }
1106 cursor.offset += text.size();
1107};
1108
1109//
1110// Utilities for insertion handling
1111//
1112
1113static bool tokenBeforeOffset(const QQmlJS::SourceLocation &t, quint32 offset)
1114{
1115 return t.end() < offset;
1116}
1117
1118static bool tokenAfterOffset(const QQmlJS::SourceLocation &t, quint32 offset)
1119{
1120 return t.begin() > offset;
1121}
1122
1123static bool insertionInsideToken(const QQmlJS::SourceLocation &token,
1124 const QQmlJS::SourceLocation &cursor)
1125{
1126 return token.begin() < cursor.begin() && token.end() >= cursor.begin();
1127}
1128
1129static bool insertionTouchesTokenLeft(const QQmlJS::SourceLocation &token,
1130 const QQmlJS::SourceLocation &cursor)
1131{
1132 return token.begin() >= cursor.begin() && token.begin() <= cursor.end();
1133}
1134
1135static void shiftTokenAfterInsert(QQmlJS::SourceLocation &t, const QQmlJS::SourceLocation &cursor,
1136 int newlines, int lastLen, int diffLen)
1137{
1138 if (t.startLine == cursor.startLine) {
1139 if (newlines > 0) {
1140 t.startColumn = lastLen + t.startColumn - cursor.startColumn + 1;
1141 } else {
1142 t.startColumn += lastLen;
1143 }
1144 }
1145 t.startLine += newlines;
1146 t.offset += diffLen;
1147}
1148
1149static void expandTokenForMiddleInsert(QQmlJS::SourceLocation &t, const QQmlLSUtils::Diff &diff,
1150 const QQmlJS::SourceLocation &cursor)
1151{
1152 auto begin = diff.text.cbegin();
1153 auto end = diff.text.cend();
1154
1155 auto ptr = std::find_if(begin, end, [](QChar c) { return c.isSpace(); });
1156
1157 if (ptr != end) {
1158 t.length = cursor.begin() - t.begin() + std::distance(begin, ptr);
1159 } else {
1160 t.length += diff.text.size();
1161 }
1162}
1163
1164static void expandTokenForLeftOverlap(QQmlJS::SourceLocation &t, const QQmlLSUtils::Diff &diff,
1165 const QQmlJS::SourceLocation &cursor, int newlines,
1166 int lastLen)
1167{
1168 const int diffLen = diff.text.size();
1169 t.offset = cursor.begin();
1170 t.length += diffLen;
1171 t.startLine = cursor.startLine;
1172 t.startColumn = cursor.startColumn;
1173
1174 // find last space inside diff text
1175 auto rbegin = diff.text.rbegin();
1176 auto rend = diff.text.rend();
1177 auto ptr = std::find_if(rbegin, rend, [](QChar c) { return c.isSpace(); });
1178
1179 if (ptr != rend) {
1180 std::ptrdiff_t omitted = std::distance(ptr, rend);
1181 t.offset += omitted;
1182 t.length -= omitted;
1183 t.startColumn += omitted;
1184 }
1185
1186 // adjust if diff contains newlines
1187 if (newlines > 0) {
1188 t.startLine += newlines;
1189 t.startColumn = lastLen - std::distance(ptr.base(), diff.text.end()) + 1;
1190 }
1191}
1192
1193static void updateHighlightsOnInsert(HighlightsContainer &highlights,
1194 QQmlJS::SourceLocation &cursor, const QQmlLSUtils::Diff &diff)
1195{
1196 const auto [newlines, lastLen] = newlineCountAndLastLineLength(diff.text);
1197 const auto diffLen = diff.text.size();
1198 cursor.length = quint32(diffLen); // set length for insertion range, used in overlap checks
1199
1200 HighlightsContainer shifted;
1201
1202 for (auto item : highlights) {
1203 auto &token = item.loc;
1204 if (tokenBeforeOffset(token, cursor.begin())) {
1205 shifted.insert(token.offset, item);
1206 continue;
1207 }
1208
1209 if (tokenAfterOffset(token, cursor.begin())) {
1210 shiftTokenAfterInsert(token, cursor, newlines, lastLen, diffLen);
1211 shifted.insert(token.offset, item);
1212 continue;
1213 }
1214
1215 // Overlap cases
1216 if (insertionInsideToken(token, cursor)) {
1217 expandTokenForMiddleInsert(token, diff, cursor);
1218 } else if (insertionTouchesTokenLeft(token, cursor)) {
1219 expandTokenForLeftOverlap(token, diff, cursor, newlines, lastLen);
1220 }
1221
1222 shifted.insert(token.offset, item);
1223 }
1224
1225 highlights.swap(shifted);
1226
1227 // Advance cursor for the next Diff
1228 updateCursorPositionByDiff(diff.text, cursor);
1229}
1230
1231//
1232// Utilities for deletion handling
1233//
1234static bool spansAcrossDeletion(const QQmlJS::SourceLocation &t, quint32 delStart, quint32 delEnd)
1235{
1236 return t.begin() < delStart && t.end() > delEnd;
1237}
1238
1239static bool leftFragmentRemains(const QQmlJS::SourceLocation &t, quint32 delStart, quint32 delEnd)
1240{
1241 return t.begin() < delStart && t.end() <= delEnd;
1242}
1243
1244static bool rightFragmentRemains(const QQmlJS::SourceLocation &t, quint32 delStart, quint32 delEnd)
1245{
1246 return t.begin() >= delStart && t.end() > delEnd;
1247}
1248
1249//
1250// Shift token after deletion
1251//
1252static void shiftTokenAfterDelete(QQmlJS::SourceLocation &t, int newlines, int lastLen,
1253 const QQmlJS::SourceLocation &cursor, int diffLen)
1254{
1255 t.offset -= diffLen;
1256
1257 // Adjust column on deletion end line
1258 if (t.startLine == cursor.startLine + newlines) {
1259 if (newlines > 0) {
1260 t.startColumn = cursor.startColumn + (t.startColumn - lastLen) - 1;
1261 } else {
1262 t.startColumn -= lastLen;
1263 }
1264 }
1265
1266 // Shift line upwards
1267 t.startLine -= newlines;
1268}
1269
1270//
1271// Apply overlap logic
1272//
1273static void applyDeletionOverlap(QQmlJS::SourceLocation &t, quint32 delStart, quint32 delEnd,
1274 int newlines, quint32 delStartLine, quint32 delStartColumn)
1275{
1276 const quint32 deletedLen = delEnd - delStart;
1277
1278 if (spansAcrossDeletion(t, delStart, delEnd)) {
1279 // Middle removed
1280 t.length -= deletedLen;
1281 return;
1282 }
1283
1284 if (leftFragmentRemains(t, delStart, delEnd)) {
1285 // Left side remains
1286 t.length = delStart - t.begin();
1287 return;
1288 }
1289
1290 if (rightFragmentRemains(t, delStart, delEnd)) {
1291 // Right side remains, shifted to the deletion start
1292 quint32 overlap = delEnd - t.begin();
1293 t.offset = delStart;
1294 t.length -= overlap;
1295
1296 t.startColumn = delStartColumn;
1297 if (newlines > 0)
1298 t.startLine = delStartLine;
1299
1300 return;
1301 }
1302
1303 // Fully removed
1304 t.length = 0;
1305}
1306
1307static void updateHighlightsOnDelete(HighlightsContainer &highlights,
1308 QQmlJS::SourceLocation &cursor, const QQmlLSUtils::Diff &diff)
1309{
1310 const auto [newlines, lastLen] = newlineCountAndLastLineLength(diff.text);
1311 const int diffLen = diff.text.size();
1312
1313 cursor.length = diffLen;
1314
1315 const quint32 delStart = cursor.offset;
1316 const quint32 delEnd = cursor.offset + diffLen;
1317
1318 HighlightsContainer shifts;
1319
1320 for (auto item : highlights) {
1321 auto &token = item.loc;
1322
1323 //
1324 // Case A: token fully before deleted region
1325 //
1326 if (tokenBeforeOffset(token, delStart)) {
1327 shifts.insert(token.offset, item);
1328 continue;
1329 }
1330
1331 //
1332 // Case B: token fully after deleted region
1333 //
1334 if (tokenAfterOffset(token, delEnd)) {
1335 shiftTokenAfterDelete(token, newlines, lastLen, cursor, diffLen);
1336 shifts.insert(token.offset, item);
1337 continue;
1338 }
1339
1340 //
1341 // Case C: deletion overlaps token
1342 //
1343 applyDeletionOverlap(token, delStart, delEnd, newlines, cursor.startLine,
1344 cursor.startColumn);
1345
1346 if (token.length == 0)
1347 continue; // fully removed
1348
1349 shifts.insert(token.offset, item);
1350 }
1351
1352 highlights.swap(shifts);
1353}
1354
1355/*
1356Equal:
1357- Just advance the running offset by length.
1358- No changes to the map.
1359
1360Insert:
1361- Insert new entries at the current offset.
1362- case A: token before insertion offset: no highlight change
1363- case B: token after insertion offset: slide all offsets forward by the length of the inserted text.
1364 sub case: if the insertion is on the same line as the token, adjust the column accordingly.
1365- case C: insertion overlaps token: expand the token length by the length of the inserted text
1366 sub case 1: insertion is inside the token: expand length
1367 sub case 2: insertion touches left of the token: adjust offset to insertion start,
1368 expand length, adjust line/column if needed.
1369
1370Delete:
1371- Case A: token before deletion offset: no highlight change
1372- case B: token after deletion offset: slide all offsets backward by the length of the deleted text.
1373 sub case: if the deletion ends on the same line as the token, adjust the column accordingly.
1374- case C: deletion overlaps token:
1375 sub case 1: spans across deletion: reduce length by deleted length
1376 sub case 2: left fragment remains: adjust length to the left fragment length
1377 sub case 3: right fragment remains: adjust offset to deletion start, adjust length to right fragment length,
1378 adjust line/column if needed.
1379 sub case 4: fully removed: remove the token from the map.
1380*/
1381void Utils::applyDiffs(HighlightsContainer &highlights, const QList<QQmlLSUtils::Diff> &diffs)
1382{
1383 using namespace QQmlLSUtils;
1384 if (highlights.isEmpty())
1385 return;
1386
1387 QQmlJS::SourceLocation cursor;
1388 cursor.offset = 0;
1389 cursor.length = 0;
1390 cursor.startLine = 1;
1391 cursor.startColumn = 1;
1392
1393 for (const Diff &diff : diffs) {
1394 switch (diff.command) {
1395 case Diff::Equal:
1396 // Just advance cursor
1397 updateCursorPositionByDiff(diff.text, cursor);
1398 break;
1399 case Diff::Insert: {
1400 updateHighlightsOnInsert(highlights, cursor, diff);
1401 break;
1402 }
1403 case Diff::Delete: {
1404 updateHighlightsOnDelete(highlights, cursor, diff);
1405 break;
1406 }
1407 }
1408 }
1409}
1410
1411} // namespace QmlHighlighting
1412
1413QT_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)
void applyDiffs(HighlightsContainer &highlights, const QList< QQmlLSUtils::Diff > &diffs)
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)
HighlightsContainer shiftHighlights(const HighlightsContainer &cachedHighlights, const QString &lastValidCode, const QString &currentCode)
bool rangeOverlapsWithSourceLocation(const QQmlJS::SourceLocation &loc, const HighlightsRange &r)
static bool rightFragmentRemains(const QQmlJS::SourceLocation &t, quint32 delStart, quint32 delEnd)
static void shiftTokenAfterInsert(QQmlJS::SourceLocation &t, const QQmlJS::SourceLocation &cursor, int newlines, int lastLen, int diffLen)
static FieldFilter highlightingFilter()
static std::pair< quint32, quint32 > newlineCountAndLastLineLength(const QString &text)
static bool insertionTouchesTokenLeft(const QQmlJS::SourceLocation &token, const QQmlJS::SourceLocation &cursor)
static void applyDeletionOverlap(QQmlJS::SourceLocation &t, quint32 delStart, quint32 delEnd, int newlines, quint32 delStartLine, quint32 delStartColumn)
static void updateCursorPositionByDiff(const QString &text, QQmlJS::SourceLocation &cursor)
static void updateHighlightsOnInsert(HighlightsContainer &highlights, QQmlJS::SourceLocation &cursor, const QQmlLSUtils::Diff &diff)
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 void expandTokenForLeftOverlap(QQmlJS::SourceLocation &t, const QQmlLSUtils::Diff &diff, const QQmlJS::SourceLocation &cursor, int newlines, int lastLen)
static bool tokenAfterOffset(const QQmlJS::SourceLocation &t, quint32 offset)
static bool insertionInsideToken(const QQmlJS::SourceLocation &token, const QQmlJS::SourceLocation &cursor)
static bool spansAcrossDeletion(const QQmlJS::SourceLocation &t, quint32 delStart, quint32 delEnd)
static void shiftTokenAfterDelete(QQmlJS::SourceLocation &t, int newlines, int lastLen, const QQmlJS::SourceLocation &cursor, int diffLen)
static int mapToProtocolForQtCreator(QmlHighlightKind highlightKind)
static void expandTokenForMiddleInsert(QQmlJS::SourceLocation &t, const QQmlLSUtils::Diff &diff, const QQmlJS::SourceLocation &cursor)
static int fromQmlModifierKindToLspTokenType(QmlHighlightModifiers highlightModifier)
static bool tokenBeforeOffset(const QQmlJS::SourceLocation &t, quint32 offset)
static bool leftFragmentRemains(const QQmlJS::SourceLocation &t, quint32 delStart, quint32 delEnd)
static void updateHighlightsOnDelete(HighlightsContainer &highlights, QQmlJS::SourceLocation &cursor, const QQmlLSUtils::Diff &diff)
static int mapToProtocolDefault(QmlHighlightKind highlightKind)
HighlightToken(const QQmlJS::SourceLocation &loc, QmlHighlightKind, QmlHighlightModifiers=QmlHighlightModifier::None)