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