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