blob: d27e5b5482623e79105bf19bc015d0825ae44843 [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/semantic_tokens/legend.dart';
import 'package:analysis_server/src/protocol/protocol_internal.dart';
import 'package:analyzer/src/test_utilities/platform.dart';
/// A helper enum to combine both standard and custom modifiers so that they
/// can all be used as DotShorthands for improved readability in test
/// expectations.
enum AllSemanticTokenModifiers {
abstract(SemanticTokenModifiers.abstract),
annotation(CustomSemanticTokenModifiers.annotation),
async(SemanticTokenModifiers.async),
constructor(CustomSemanticTokenModifiers.constructor),
control(CustomSemanticTokenModifiers.control),
declaration(SemanticTokenModifiers.declaration),
defaultLibrary(SemanticTokenModifiers.defaultLibrary),
definition(SemanticTokenModifiers.definition),
deprecated(SemanticTokenModifiers.deprecated),
documentation(SemanticTokenModifiers.documentation),
escape(CustomSemanticTokenModifiers.escape),
importPrefix(CustomSemanticTokenModifiers.importPrefix),
instance(CustomSemanticTokenModifiers.instance),
interpolation(CustomSemanticTokenModifiers.interpolation),
label(CustomSemanticTokenModifiers.label),
modification(SemanticTokenModifiers.modification),
readonly(SemanticTokenModifiers.readonly),
source(CustomSemanticTokenModifiers.source),
static(SemanticTokenModifiers.static),
void_(CustomSemanticTokenModifiers.void_),
wildcard(CustomSemanticTokenModifiers.wildcard);
final SemanticTokenModifiers modifier;
new(this.modifier);
factory forModifier(SemanticTokenModifiers modifier) {
return AllSemanticTokenModifiers.values.singleWhere(
(value) => value.modifier == modifier,
);
}
}
/// A helper enum to combine both standard and custom semantic token types so
/// that they can all be used as DotShorthands for improved readability in test
/// expectations.
///
///
enum AllSemanticTokenTypes {
annotation(CustomSemanticTokenTypes.annotation),
boolean(CustomSemanticTokenTypes.boolean),
class_(SemanticTokenTypes.class_),
comment(SemanticTokenTypes.comment),
decorator(SemanticTokenTypes.decorator),
enum_(SemanticTokenTypes.enum_),
enumMember(SemanticTokenTypes.enumMember),
event(SemanticTokenTypes.event),
function(SemanticTokenTypes.function),
interface(SemanticTokenTypes.interface),
keyword(SemanticTokenTypes.keyword),
label(SemanticTokenTypes.label),
macro(SemanticTokenTypes.macro),
method(SemanticTokenTypes.method),
modifier(SemanticTokenTypes.modifier),
namespace(SemanticTokenTypes.namespace),
number(SemanticTokenTypes.number),
operator(SemanticTokenTypes.operator),
parameter(SemanticTokenTypes.parameter),
property(SemanticTokenTypes.property),
regexp(SemanticTokenTypes.regexp),
source(CustomSemanticTokenTypes.source),
string(SemanticTokenTypes.string),
struct(SemanticTokenTypes.struct),
type(SemanticTokenTypes.type),
typeParameter(SemanticTokenTypes.typeParameter),
variable(SemanticTokenTypes.variable);
final SemanticTokenTypes tokenType;
new(this.tokenType);
factory forTokenType(SemanticTokenTypes tokenType) {
return AllSemanticTokenTypes.values.singleWhere(
(value) => value.tokenType == tokenType,
);
}
}
mixin SemanticTokensTestMixin {
SemanticTokenLegendLookup get serverProvidedSemanticTokenLegend;
/// Decode tokens according to the LSP spec and pair with relevant file contents.
List<Token> decodeSemanticTokens(String content, SemanticTokens tokens) {
var contentLines = content
.split(testEol)
.map((line) => '$line$testEol')
.toList();
var results = <Token>[];
var lastLine = 0;
var lastColumn = 0;
for (var i = 0; i < tokens.data.length; i += 5) {
var lineDelta = tokens.data[i];
var columnDelta = tokens.data[i + 1];
var length = tokens.data[i + 2];
var tokenTypeIndex = tokens.data[i + 3];
var modifierBitmask = tokens.data[i + 4];
// Calculate the actual line/col from the deltas.
var line = lastLine + lineDelta;
var column = lineDelta == 0 ? lastColumn + columnDelta : columnDelta;
var tokenContent = contentLines[line].substring(column, column + length);
results.add(
Token(
tokenContent,
AllSemanticTokenTypes.forTokenType(
serverProvidedSemanticTokenLegend.typeForIndex(tokenTypeIndex),
),
serverProvidedSemanticTokenLegend
.modifiersForBitmask(modifierBitmask)
.map(AllSemanticTokenModifiers.forModifier)
.toList(),
),
);
lastLine = line;
lastColumn = column;
}
return results;
}
}
class Token {
final String content;
final SemanticTokenTypes type;
final List<SemanticTokenModifiers> modifiers;
new(
this.content,
AllSemanticTokenTypes type, [
List<AllSemanticTokenModifiers> mods = const [],
]) : type = type.tokenType,
modifiers = mods.map((mod) => mod.modifier).toList();
@override
int get hashCode => content.hashCode;
@override
bool operator ==(Object o) =>
o is Token &&
o.content == content &&
o.type == type &&
listEqual(
// Treat nulls the same as empty lists for convenience when comparing.
o.modifiers,
modifiers,
(SemanticTokenModifiers a, SemanticTokenModifiers b) => a == b,
);
/// Outputs a text representation of the token in the form of constructor
/// args for easy copy/pasting into tests to update expectations.
@override
String toString() {
var modifiersString = modifiers.isEmpty
? ''
: ', [${modifiers.map((m) => '.$m').join(', ')}]';
return "('$content', .$type$modifiersString)";
}
}