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