From fe8c602f6aaaf7f6ea8d81c8be96342763491eef Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Fri, 20 Oct 2023 16:03:25 +0200 Subject: [PATCH] feat: semantic highlighting (#653) Closes #27 ### Summary of Changes Add semantic highlighting for declaration names and names used in cross-references. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- docs/lexer/safe_ds_lexer/_safe_ds_lexer.py | 3 +- package.json | 1 + src/language/builtins/safe-ds-classes.ts | 24 ++ src/language/grammar/safe-ds.langium | 2 +- .../{formatting => lsp}/safe-ds-formatter.ts | 0 .../lsp/safe-ds-semantic-token-provider.ts | 226 ++++++++++++++++++ src/language/safe-ds-module.ts | 4 +- .../validation/builtins/deprecated.ts | 8 +- .../validation/builtins/experimental.ts | 2 +- .../validation/builtins/repeatable.ts | 2 +- .../other/declarations/annotationCalls.ts | 4 +- .../other/declarations/placeholders.ts | 2 + .../validation/other/declarations/segments.ts | 2 + .../language/{ => lsp}/formatting/creator.ts | 8 +- .../formatting/testFormatting.test.ts | 2 +- .../safe-ds-semantic-token-provider.test.ts | 189 +++++++++++++++ 16 files changed, 466 insertions(+), 13 deletions(-) rename src/language/{formatting => lsp}/safe-ds-formatter.ts (100%) create mode 100644 src/language/lsp/safe-ds-semantic-token-provider.ts rename tests/language/{ => lsp}/formatting/creator.ts (93%) rename tests/language/{ => lsp}/formatting/testFormatting.test.ts (94%) create mode 100644 tests/language/lsp/safe-ds-semantic-token-provider.test.ts diff --git a/docs/lexer/safe_ds_lexer/_safe_ds_lexer.py b/docs/lexer/safe_ds_lexer/_safe_ds_lexer.py index 1b149128a..b6860ea96 100644 --- a/docs/lexer/safe_ds_lexer/_safe_ds_lexer.py +++ b/docs/lexer/safe_ds_lexer/_safe_ds_lexer.py @@ -61,7 +61,8 @@ "Number", "Int", "Float", - "ListMap", + "List", + "Map", "String", ) diff --git a/package.json b/package.json index f737f7199..863774ccf 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ ], "configurationDefaults": { "[safe-ds]": { + "editor.semanticHighlighting.enabled": true, "editor.wordSeparators": "`~!@#$%^&*()-=+[]{}\\|;:'\",.<>/?»«", "files.trimTrailingWhitespace": true } diff --git a/src/language/builtins/safe-ds-classes.ts b/src/language/builtins/safe-ds-classes.ts index 016f98c4d..ea7d96f84 100644 --- a/src/language/builtins/safe-ds-classes.ts +++ b/src/language/builtins/safe-ds-classes.ts @@ -33,10 +33,34 @@ export class SafeDsClasses extends SafeDsModuleMembers { return this.getClass('Nothing'); } + get Number(): SdsClass | undefined { + return this.getClass('Number'); + } + get String(): SdsClass | undefined { return this.getClass('String'); } + /** + * Returns whether the given node is a builtin class. + */ + isBuiltinClass(node: SdsClass | undefined): boolean { + return ( + Boolean(node) && + [ + this.Any, + this.Boolean, + this.Float, + this.Int, + this.List, + this.Map, + this.Nothing, + this.Number, + this.String, + ].includes(node) + ); + } + private getClass(name: string): SdsClass | undefined { return this.getModuleMember(CORE_CLASSES_URI, name, isSdsClass); } diff --git a/src/language/grammar/safe-ds.langium b/src/language/grammar/safe-ds.langium index d80bd5e23..291c631c3 100644 --- a/src/language/grammar/safe-ds.langium +++ b/src/language/grammar/safe-ds.langium @@ -342,7 +342,7 @@ fragment SdsSegmentFragment: interface SdsAnnotationCallList extends SdsAnnotatedObject {} interface SdsAnnotationCall extends SdsAbstractCall { - annotation: @SdsAnnotation + annotation?: @SdsAnnotation } SdsAnnotationCall returns SdsAnnotationCall: diff --git a/src/language/formatting/safe-ds-formatter.ts b/src/language/lsp/safe-ds-formatter.ts similarity index 100% rename from src/language/formatting/safe-ds-formatter.ts rename to src/language/lsp/safe-ds-formatter.ts diff --git a/src/language/lsp/safe-ds-semantic-token-provider.ts b/src/language/lsp/safe-ds-semantic-token-provider.ts new file mode 100644 index 000000000..d315c77d4 --- /dev/null +++ b/src/language/lsp/safe-ds-semantic-token-provider.ts @@ -0,0 +1,226 @@ +import { + AbstractSemanticTokenProvider, + AllSemanticTokenTypes, + AstNode, + hasContainerOfType, + SemanticTokenAcceptor, + DefaultSemanticTokenOptions, +} from 'langium'; +import { + isSdsAnnotation, + isSdsAnnotationCall, + isSdsArgument, + isSdsAttribute, + isSdsClass, + isSdsDeclaration, + isSdsEnum, + isSdsEnumVariant, + isSdsFunction, + isSdsImport, + isSdsImportedDeclaration, + isSdsModule, + isSdsNamedType, + isSdsParameter, + isSdsPipeline, + isSdsPlaceholder, + isSdsReference, + isSdsSegment, + isSdsTypeArgument, + isSdsTypeParameter, + isSdsTypeParameterConstraint, +} from '../generated/ast.js'; +import { SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver-types'; +import { SafeDsServices } from '../safe-ds-module.js'; +import { SafeDsClasses } from '../builtins/safe-ds-classes.js'; + +// Add a new semantic token type for decorators, which is missing in langium v2.0.2 +if (!AllSemanticTokenTypes[SemanticTokenTypes.decorator]) { + const maxValue = Math.max(...Object.values(AllSemanticTokenTypes)); + AllSemanticTokenTypes[SemanticTokenTypes.decorator] = maxValue + 1; + + DefaultSemanticTokenOptions.legend.tokenTypes = Object.keys(AllSemanticTokenTypes); +} + +export class SafeDsSemanticTokenProvider extends AbstractSemanticTokenProvider { + private readonly builtinClasses: SafeDsClasses; + + constructor(services: SafeDsServices) { + super(services); + + this.builtinClasses = services.builtins.Classes; + } + + protected highlightElement(node: AstNode, acceptor: SemanticTokenAcceptor): void { + if (isSdsAnnotationCall(node)) { + acceptor({ + node, + keyword: '@', + type: SemanticTokenTypes.decorator, + }); + acceptor({ + node, + property: 'annotation', + type: SemanticTokenTypes.decorator, + }); + } else if (isSdsArgument(node)) { + if (node.parameter) { + acceptor({ + node, + property: 'parameter', + type: SemanticTokenTypes.parameter, + }); + } + } else if (isSdsDeclaration(node)) { + const info = this.computeSemanticTokenInfoForDeclaration(node, [SemanticTokenModifiers.declaration]); + if (info) { + acceptor({ + node, + property: 'name', + ...info, + }); + } + } else if (isSdsImport(node)) { + acceptor({ + node, + property: 'package', + type: SemanticTokenTypes.namespace, + }); + } else if (isSdsImportedDeclaration(node)) { + const info = this.computeSemanticTokenInfoForDeclaration(node.declaration.ref); + if (info) { + acceptor({ + node, + property: 'declaration', + ...info, + }); + } + } else if (isSdsNamedType(node)) { + const info = this.computeSemanticTokenInfoForDeclaration(node.declaration.ref); + if (info) { + acceptor({ + node, + property: 'declaration', + ...info, + }); + } + } else if (isSdsReference(node)) { + const info = this.computeSemanticTokenInfoForDeclaration(node.target.ref); + if (info) { + acceptor({ + node, + property: 'target', + ...info, + }); + } + } else if (isSdsTypeArgument(node)) { + if (node.typeParameter) { + acceptor({ + node, + property: 'typeParameter', + type: SemanticTokenTypes.typeParameter, + }); + } + } else if (isSdsTypeParameterConstraint(node)) { + acceptor({ + node, + property: 'leftOperand', + type: SemanticTokenTypes.typeParameter, + }); + } + } + + private computeSemanticTokenInfoForDeclaration( + node: AstNode | undefined, + additionalModifiers: SemanticTokenModifiers[] = [], + ): SemanticTokenInfo | void { + /* c8 ignore start */ + if (!node) { + return; + } + /* c8 ignore stop */ + + if (isSdsAnnotation(node)) { + return { + type: SemanticTokenTypes.decorator, + modifier: additionalModifiers, + }; + } else if (isSdsAttribute(node)) { + const modifier = [SemanticTokenModifiers.readonly, ...additionalModifiers]; + if (node.isStatic) { + modifier.push(SemanticTokenModifiers.static); + } + + return { + type: SemanticTokenTypes.property, + modifier, + }; + } else if (isSdsClass(node)) { + const isBuiltinClass = this.builtinClasses.isBuiltinClass(node); + return { + type: SemanticTokenTypes.class, + modifier: isBuiltinClass + ? [SemanticTokenModifiers.defaultLibrary, ...additionalModifiers] + : additionalModifiers, + }; + } else if (isSdsEnum(node)) { + return { + type: SemanticTokenTypes.enum, + modifier: additionalModifiers, + }; + } else if (isSdsEnumVariant(node)) { + return { + type: SemanticTokenTypes.enumMember, + modifier: additionalModifiers, + }; + } else if (isSdsFunction(node)) { + if (hasContainerOfType(node, isSdsClass)) { + return { + type: SemanticTokenTypes.method, + modifier: node.isStatic + ? [SemanticTokenModifiers.static, ...additionalModifiers] + : additionalModifiers, + }; + } else { + return { + type: SemanticTokenTypes.function, + modifier: additionalModifiers, + }; + } + } else if (isSdsModule(node)) { + return { + type: SemanticTokenTypes.namespace, + modifier: additionalModifiers, + }; + } else if (isSdsParameter(node)) { + return { + type: SemanticTokenTypes.parameter, + modifier: additionalModifiers, + }; + } else if (isSdsPipeline(node)) { + return { + type: SemanticTokenTypes.function, + modifier: additionalModifiers, + }; + } else if (isSdsPlaceholder(node)) { + return { + type: SemanticTokenTypes.variable, + modifier: [SemanticTokenModifiers.readonly, ...additionalModifiers], + }; + } else if (isSdsSegment(node)) { + return { + type: SemanticTokenTypes.function, + modifier: additionalModifiers, + }; + } else if (isSdsTypeParameter(node)) { + return { + type: SemanticTokenTypes.typeParameter, + modifier: additionalModifiers, + }; + } + } +} + +interface SemanticTokenInfo { + type: SemanticTokenTypes; + modifier?: SemanticTokenModifiers | SemanticTokenModifiers[]; +} diff --git a/src/language/safe-ds-module.ts b/src/language/safe-ds-module.ts index 524fd4473..3c16c02db 100644 --- a/src/language/safe-ds-module.ts +++ b/src/language/safe-ds-module.ts @@ -11,7 +11,7 @@ import { } from 'langium'; import { SafeDsGeneratedModule, SafeDsGeneratedSharedModule } from './generated/module.js'; import { registerValidationChecks } from './validation/safe-ds-validator.js'; -import { SafeDsFormatter } from './formatting/safe-ds-formatter.js'; +import { SafeDsFormatter } from './lsp/safe-ds-formatter.js'; import { SafeDsWorkspaceManager } from './workspace/safe-ds-workspace-manager.js'; import { SafeDsScopeComputation } from './scoping/safe-ds-scope-computation.js'; import { SafeDsScopeProvider } from './scoping/safe-ds-scope-provider.js'; @@ -23,6 +23,7 @@ import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js'; import { SafeDsAnnotations } from './builtins/safe-ds-annotations.js'; import { SafeDsClassHierarchy } from './typing/safe-ds-class-hierarchy.js'; import { SafeDsPartialEvaluator } from './partialEvaluation/safe-ds-partial-evaluator.js'; +import { SafeDsSemanticTokenProvider } from './lsp/safe-ds-semantic-token-provider.js'; /** * Declaration of custom services - add your own service classes here. @@ -71,6 +72,7 @@ export const SafeDsModule: Module new SafeDsFormatter(), + SemanticTokenProvider: (services) => new SafeDsSemanticTokenProvider(services), }, parser: { ValueConverter: () => new SafeDsValueConverter(), diff --git a/src/language/validation/builtins/deprecated.ts b/src/language/validation/builtins/deprecated.ts index 208a0c468..29a314408 100644 --- a/src/language/validation/builtins/deprecated.ts +++ b/src/language/validation/builtins/deprecated.ts @@ -13,6 +13,7 @@ import { import { SafeDsServices } from '../../safe-ds-module.js'; import { isRequiredParameter } from '../../helpers/nodeProperties.js'; import { parameterCanBeAnnotated } from '../other/declarations/annotationCalls.js'; +import { DiagnosticTag } from 'vscode-languageserver-types'; export const CODE_DEPRECATED_ASSIGNED_RESULT = 'deprecated/assigned-result'; export const CODE_DEPRECATED_CALLED_ANNOTATION = 'deprecated/called-annotation'; @@ -35,13 +36,14 @@ export const assigneeAssignedResultShouldNotBeDeprecated = accept('warning', `The assigned result '${assignedObject.name}' is deprecated.`, { node, code: CODE_DEPRECATED_ASSIGNED_RESULT, + tags: [DiagnosticTag.Deprecated], }); } }; export const annotationCallAnnotationShouldNotBeDeprecated = (services: SafeDsServices) => (node: SdsAnnotationCall, accept: ValidationAcceptor) => { - const annotation = node.annotation.ref; + const annotation = node.annotation?.ref; if (!annotation) { return; } @@ -51,6 +53,7 @@ export const annotationCallAnnotationShouldNotBeDeprecated = node, property: 'annotation', code: CODE_DEPRECATED_CALLED_ANNOTATION, + tags: [DiagnosticTag.Deprecated], }); } }; @@ -66,6 +69,7 @@ export const argumentCorrespondingParameterShouldNotBeDeprecated = accept('warning', `The corresponding parameter '${parameter.name}' is deprecated.`, { node, code: CODE_DEPRECATED_CORRESPONDING_PARAMETER, + tags: [DiagnosticTag.Deprecated], }); } }; @@ -81,6 +85,7 @@ export const namedTypeDeclarationShouldNotBeDeprecated = accept('warning', `The referenced declaration '${declaration.name}' is deprecated.`, { node, code: CODE_DEPRECATED_REFERENCED_DECLARATION, + tags: [DiagnosticTag.Deprecated], }); } }; @@ -96,6 +101,7 @@ export const referenceTargetShouldNotBeDeprecated = accept('warning', `The referenced declaration '${target.name}' is deprecated.`, { node, code: CODE_DEPRECATED_REFERENCED_DECLARATION, + tags: [DiagnosticTag.Deprecated], }); } }; diff --git a/src/language/validation/builtins/experimental.ts b/src/language/validation/builtins/experimental.ts index 09b28618d..6e973effb 100644 --- a/src/language/validation/builtins/experimental.ts +++ b/src/language/validation/builtins/experimental.ts @@ -37,7 +37,7 @@ export const assigneeAssignedResultShouldNotBeExperimental = export const annotationCallAnnotationShouldNotBeExperimental = (services: SafeDsServices) => (node: SdsAnnotationCall, accept: ValidationAcceptor) => { - const annotation = node.annotation.ref; + const annotation = node.annotation?.ref; if (!annotation) { return; } diff --git a/src/language/validation/builtins/repeatable.ts b/src/language/validation/builtins/repeatable.ts index 14aef30b9..2cb75eac7 100644 --- a/src/language/validation/builtins/repeatable.ts +++ b/src/language/validation/builtins/repeatable.ts @@ -14,7 +14,7 @@ export const singleUseAnnotationsMustNotBeRepeated = }); for (const duplicate of duplicatesBy(callsOfSingleUseAnnotations, (it) => it.annotation?.ref)) { - accept('error', `The annotation '${duplicate.annotation.$refText}' is not repeatable.`, { + accept('error', `The annotation '${duplicate.annotation?.$refText}' is not repeatable.`, { node: duplicate, property: 'annotation', code: CODE_ANNOTATION_NOT_REPEATABLE, diff --git a/src/language/validation/other/declarations/annotationCalls.ts b/src/language/validation/other/declarations/annotationCalls.ts index 103a6e755..84e4fbddc 100644 --- a/src/language/validation/other/declarations/annotationCalls.ts +++ b/src/language/validation/other/declarations/annotationCalls.ts @@ -46,11 +46,11 @@ export const annotationCallMustNotLackArgumentList = (node: SdsAnnotationCall, a return; } - const requiredParameters = parametersOrEmpty(node.annotation.ref).filter(isRequiredParameter); + const requiredParameters = parametersOrEmpty(node.annotation?.ref).filter(isRequiredParameter); if (!isEmpty(requiredParameters)) { accept( 'error', - `The annotation '${node.annotation.$refText}' has required parameters, so an argument list must be added.`, + `The annotation '${node.annotation?.$refText}' has required parameters, so an argument list must be added.`, { node, code: CODE_ANNOTATION_CALL_MISSING_ARGUMENT_LIST, diff --git a/src/language/validation/other/declarations/placeholders.ts b/src/language/validation/other/declarations/placeholders.ts index 519f912db..2fbf3d462 100644 --- a/src/language/validation/other/declarations/placeholders.ts +++ b/src/language/validation/other/declarations/placeholders.ts @@ -11,6 +11,7 @@ import { getContainerOfType, ValidationAcceptor } from 'langium'; import { SafeDsServices } from '../../../safe-ds-module.js'; import { statementsOrEmpty } from '../../../helpers/nodeProperties.js'; import { last } from 'radash'; +import { DiagnosticTag } from 'vscode-languageserver-types'; export const CODE_PLACEHOLDER_ALIAS = 'placeholder/alias'; export const CODE_PLACEHOLDER_UNUSED = 'placeholder/unused'; @@ -55,5 +56,6 @@ export const placeholderShouldBeUsed = node, property: 'name', code: CODE_PLACEHOLDER_UNUSED, + tags: [DiagnosticTag.Unnecessary], }); }; diff --git a/src/language/validation/other/declarations/segments.ts b/src/language/validation/other/declarations/segments.ts index 8c0bf1ffc..0f2ae329f 100644 --- a/src/language/validation/other/declarations/segments.ts +++ b/src/language/validation/other/declarations/segments.ts @@ -2,6 +2,7 @@ import { SdsSegment } from '../../../generated/ast.js'; import { ValidationAcceptor } from 'langium'; import { parametersOrEmpty, resultsOrEmpty } from '../../../helpers/nodeProperties.js'; import { SafeDsServices } from '../../../safe-ds-module.js'; +import { DiagnosticTag } from 'vscode-languageserver-types'; export const CODE_SEGMENT_DUPLICATE_YIELD = 'segment/duplicate-yield'; export const CODE_SEGMENT_UNASSIGNED_RESULT = 'segment/unassigned-result'; @@ -42,6 +43,7 @@ export const segmentParameterShouldBeUsed = node: parameter, property: 'name', code: CODE_SEGMENT_UNUSED_PARAMETER, + tags: [DiagnosticTag.Unnecessary], }); } } diff --git a/tests/language/formatting/creator.ts b/tests/language/lsp/formatting/creator.ts similarity index 93% rename from tests/language/formatting/creator.ts rename to tests/language/lsp/formatting/creator.ts index 1626e435d..db86efa2a 100644 --- a/tests/language/formatting/creator.ts +++ b/tests/language/lsp/formatting/creator.ts @@ -1,10 +1,10 @@ -import { listTestSafeDsFiles, uriToShortenedTestResourceName } from '../../helpers/testResources.js'; +import { listTestSafeDsFiles, uriToShortenedTestResourceName } from '../../../helpers/testResources.js'; import fs from 'fs'; import { Diagnostic } from 'vscode-languageserver-types'; -import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { createSafeDsServices } from '../../../../src/language/safe-ds-module.js'; import { EmptyFileSystem, URI } from 'langium'; -import { getSyntaxErrors } from '../../helpers/diagnostics.js'; -import { TestDescription, TestDescriptionError } from '../../helpers/testDescription.js'; +import { getSyntaxErrors } from '../../../helpers/diagnostics.js'; +import { TestDescription, TestDescriptionError } from '../../../helpers/testDescription.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; const rootResourceName = 'formatting'; diff --git a/tests/language/formatting/testFormatting.test.ts b/tests/language/lsp/formatting/testFormatting.test.ts similarity index 94% rename from tests/language/formatting/testFormatting.test.ts rename to tests/language/lsp/formatting/testFormatting.test.ts index 5b5fc4c22..1f40ab31b 100644 --- a/tests/language/formatting/testFormatting.test.ts +++ b/tests/language/lsp/formatting/testFormatting.test.ts @@ -1,4 +1,4 @@ -import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { createSafeDsServices } from '../../../../src/language/safe-ds-module.js'; import { clearDocuments, expectFormatting } from 'langium/test'; import { afterEach, describe, it } from 'vitest'; import { EmptyFileSystem } from 'langium'; diff --git a/tests/language/lsp/safe-ds-semantic-token-provider.test.ts b/tests/language/lsp/safe-ds-semantic-token-provider.test.ts new file mode 100644 index 000000000..cf3c694bd --- /dev/null +++ b/tests/language/lsp/safe-ds-semantic-token-provider.test.ts @@ -0,0 +1,189 @@ +import { afterEach, beforeEach, describe, it } from 'vitest'; +import { clearDocuments, highlightHelper } from 'langium/test'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { SemanticTokenTypes } from 'vscode-languageserver-types'; +import { AssertionError } from 'assert'; +import { NodeFileSystem } from 'langium/node'; + +const services = createSafeDsServices(NodeFileSystem).SafeDs; + +describe('SafeDsSemanticTokenProvider', async () => { + beforeEach(async () => { + // Load the builtin library + await services.shared.workspace.WorkspaceManager.initializeWorkspace([]); + }); + + afterEach(async () => { + await clearDocuments(services); + }); + + it.each([ + { + testName: 'annotation call', + code: '<|@|><|A|>', + expectedTokenTypes: [SemanticTokenTypes.decorator], + }, + { + testName: 'argument', + code: ` + fun f(p: String) + + pipeline p { + f(<|p|> = "foo") + } + `, + expectedTokenTypes: [SemanticTokenTypes.parameter], + }, + { + testName: 'annotation declaration', + code: 'annotation <|A|>', + expectedTokenTypes: [SemanticTokenTypes.decorator], + }, + { + testName: 'attribute declaration', + code: ` + class C { + attr <|a|> + static attr <|b|> + } + `, + expectedTokenTypes: [SemanticTokenTypes.property, SemanticTokenTypes.property], + }, + { + testName: 'class declaration', + code: 'class <|C|>', + expectedTokenTypes: [SemanticTokenTypes.class], + }, + { + testName: 'enum declaration', + code: 'enum <|E|>', + expectedTokenTypes: [SemanticTokenTypes.enum], + }, + { + testName: 'enum variant declaration', + code: 'enum E { <|V|> }', + expectedTokenTypes: [SemanticTokenTypes.enumMember], + }, + { + testName: 'function declaration', + code: ` + class C { + fun <|f|>() + static fun <|g|>() + } + + fun <|f|>() + `, + expectedTokenTypes: [SemanticTokenTypes.method, SemanticTokenTypes.method, SemanticTokenTypes.function], + }, + { + testName: 'module', + code: 'package <|a.b.c|>', + expectedTokenTypes: [SemanticTokenTypes.namespace], + }, + { + testName: 'parameter declaration', + code: 'fun f(<|p|>: String)', + expectedTokenTypes: [SemanticTokenTypes.parameter], + }, + { + testName: 'pipeline declaration', + code: 'pipeline <|p|> {}', + expectedTokenTypes: [SemanticTokenTypes.function], + }, + { + testName: 'placeholder declaration', + code: ` + pipeline p { + val <|a|> = 1; + } + `, + expectedTokenTypes: [SemanticTokenTypes.variable], + }, + { + testName: 'segment declaration', + code: 'segment <|s|>() {}', + expectedTokenTypes: [SemanticTokenTypes.function], + }, + { + testName: 'type parameter declaration', + code: 'class C<<|T|>>', + expectedTokenTypes: [SemanticTokenTypes.typeParameter], + }, + { + testName: 'import', + code: 'from <|a.b.c|> import X', + expectedTokenTypes: [SemanticTokenTypes.namespace], + }, + { + testName: 'imported declaration', + code: 'from safeds.lang import <|Any|>', + expectedTokenTypes: [SemanticTokenTypes.class], + }, + { + testName: 'named type', + code: ` + enum E {} + + fun f(p: <|E|>) + `, + expectedTokenTypes: [SemanticTokenTypes.enum], + }, + { + testName: 'reference', + code: ` + fun f(p: String) + + pipeline p { + <|f|>; + } + `, + expectedTokenTypes: [SemanticTokenTypes.function], + }, + { + testName: 'type argument', + code: ` + class C + + fun f(p: C<<|T|> = C>) + `, + expectedTokenTypes: [SemanticTokenTypes.typeParameter], + }, + { + testName: 'type parameter constraint', + code: ` + class C where { + <|T|> sub C + } + `, + expectedTokenTypes: [SemanticTokenTypes.typeParameter], + }, + ])('should assign the correct token types ($testName)', async ({ code, expectedTokenTypes }) => { + await checkSemanticTokens(code, expectedTokenTypes); + }); +}); + +const checkSemanticTokens = async (code: string, expectedTokenTypes: SemanticTokenTypes[]) => { + const tokensWithRanges = await highlightHelper(services)(code); + expectedTokenTypes.forEach((expectedTokenType, index) => { + const range = tokensWithRanges.ranges[index]; + const tokensAtRange = tokensWithRanges.tokens.filter( + (token) => token.offset === range[0] && token.offset + token.text.length === range[1], + ); + + if (tokensAtRange.length !== 1) { + throw new AssertionError({ + message: `Expected exactly one token at offset range ${range}, but found ${tokensAtRange.length}`, + }); + } + + const tokenAtRange = tokensAtRange[0]; + if (tokenAtRange.tokenType !== expectedTokenType) { + throw new AssertionError({ + message: `Expected token at offset range ${range} to be of type ${expectedTokenType}, but was ${tokenAtRange.tokenType}`, + actual: tokenAtRange.tokenType, + expected: expectedTokenType, + }); + } + }); +};