Skip to content

Commit fe8c602

Browse files
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>
1 parent c59856c commit fe8c602

16 files changed

+466
-13
lines changed

docs/lexer/safe_ds_lexer/_safe_ds_lexer.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
"Number",
6262
"Int",
6363
"Float",
64-
"ListMap",
64+
"List",
65+
"Map",
6566
"String",
6667
)
6768

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
],
7777
"configurationDefaults": {
7878
"[safe-ds]": {
79+
"editor.semanticHighlighting.enabled": true,
7980
"editor.wordSeparators": "`~!@#$%^&*()-=+[]{}\\|;:'\",.<>/?»«",
8081
"files.trimTrailingWhitespace": true
8182
}

src/language/builtins/safe-ds-classes.ts

+24
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,34 @@ export class SafeDsClasses extends SafeDsModuleMembers<SdsClass> {
3333
return this.getClass('Nothing');
3434
}
3535

36+
get Number(): SdsClass | undefined {
37+
return this.getClass('Number');
38+
}
39+
3640
get String(): SdsClass | undefined {
3741
return this.getClass('String');
3842
}
3943

44+
/**
45+
* Returns whether the given node is a builtin class.
46+
*/
47+
isBuiltinClass(node: SdsClass | undefined): boolean {
48+
return (
49+
Boolean(node) &&
50+
[
51+
this.Any,
52+
this.Boolean,
53+
this.Float,
54+
this.Int,
55+
this.List,
56+
this.Map,
57+
this.Nothing,
58+
this.Number,
59+
this.String,
60+
].includes(node)
61+
);
62+
}
63+
4064
private getClass(name: string): SdsClass | undefined {
4165
return this.getModuleMember(CORE_CLASSES_URI, name, isSdsClass);
4266
}

src/language/grammar/safe-ds.langium

+1-1
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ fragment SdsSegmentFragment:
342342
interface SdsAnnotationCallList extends SdsAnnotatedObject {}
343343

344344
interface SdsAnnotationCall extends SdsAbstractCall {
345-
annotation: @SdsAnnotation
345+
annotation?: @SdsAnnotation
346346
}
347347

348348
SdsAnnotationCall returns SdsAnnotationCall:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import {
2+
AbstractSemanticTokenProvider,
3+
AllSemanticTokenTypes,
4+
AstNode,
5+
hasContainerOfType,
6+
SemanticTokenAcceptor,
7+
DefaultSemanticTokenOptions,
8+
} from 'langium';
9+
import {
10+
isSdsAnnotation,
11+
isSdsAnnotationCall,
12+
isSdsArgument,
13+
isSdsAttribute,
14+
isSdsClass,
15+
isSdsDeclaration,
16+
isSdsEnum,
17+
isSdsEnumVariant,
18+
isSdsFunction,
19+
isSdsImport,
20+
isSdsImportedDeclaration,
21+
isSdsModule,
22+
isSdsNamedType,
23+
isSdsParameter,
24+
isSdsPipeline,
25+
isSdsPlaceholder,
26+
isSdsReference,
27+
isSdsSegment,
28+
isSdsTypeArgument,
29+
isSdsTypeParameter,
30+
isSdsTypeParameterConstraint,
31+
} from '../generated/ast.js';
32+
import { SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver-types';
33+
import { SafeDsServices } from '../safe-ds-module.js';
34+
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';
35+
36+
// Add a new semantic token type for decorators, which is missing in langium v2.0.2
37+
if (!AllSemanticTokenTypes[SemanticTokenTypes.decorator]) {
38+
const maxValue = Math.max(...Object.values(AllSemanticTokenTypes));
39+
AllSemanticTokenTypes[SemanticTokenTypes.decorator] = maxValue + 1;
40+
41+
DefaultSemanticTokenOptions.legend.tokenTypes = Object.keys(AllSemanticTokenTypes);
42+
}
43+
44+
export class SafeDsSemanticTokenProvider extends AbstractSemanticTokenProvider {
45+
private readonly builtinClasses: SafeDsClasses;
46+
47+
constructor(services: SafeDsServices) {
48+
super(services);
49+
50+
this.builtinClasses = services.builtins.Classes;
51+
}
52+
53+
protected highlightElement(node: AstNode, acceptor: SemanticTokenAcceptor): void {
54+
if (isSdsAnnotationCall(node)) {
55+
acceptor({
56+
node,
57+
keyword: '@',
58+
type: SemanticTokenTypes.decorator,
59+
});
60+
acceptor({
61+
node,
62+
property: 'annotation',
63+
type: SemanticTokenTypes.decorator,
64+
});
65+
} else if (isSdsArgument(node)) {
66+
if (node.parameter) {
67+
acceptor({
68+
node,
69+
property: 'parameter',
70+
type: SemanticTokenTypes.parameter,
71+
});
72+
}
73+
} else if (isSdsDeclaration(node)) {
74+
const info = this.computeSemanticTokenInfoForDeclaration(node, [SemanticTokenModifiers.declaration]);
75+
if (info) {
76+
acceptor({
77+
node,
78+
property: 'name',
79+
...info,
80+
});
81+
}
82+
} else if (isSdsImport(node)) {
83+
acceptor({
84+
node,
85+
property: 'package',
86+
type: SemanticTokenTypes.namespace,
87+
});
88+
} else if (isSdsImportedDeclaration(node)) {
89+
const info = this.computeSemanticTokenInfoForDeclaration(node.declaration.ref);
90+
if (info) {
91+
acceptor({
92+
node,
93+
property: 'declaration',
94+
...info,
95+
});
96+
}
97+
} else if (isSdsNamedType(node)) {
98+
const info = this.computeSemanticTokenInfoForDeclaration(node.declaration.ref);
99+
if (info) {
100+
acceptor({
101+
node,
102+
property: 'declaration',
103+
...info,
104+
});
105+
}
106+
} else if (isSdsReference(node)) {
107+
const info = this.computeSemanticTokenInfoForDeclaration(node.target.ref);
108+
if (info) {
109+
acceptor({
110+
node,
111+
property: 'target',
112+
...info,
113+
});
114+
}
115+
} else if (isSdsTypeArgument(node)) {
116+
if (node.typeParameter) {
117+
acceptor({
118+
node,
119+
property: 'typeParameter',
120+
type: SemanticTokenTypes.typeParameter,
121+
});
122+
}
123+
} else if (isSdsTypeParameterConstraint(node)) {
124+
acceptor({
125+
node,
126+
property: 'leftOperand',
127+
type: SemanticTokenTypes.typeParameter,
128+
});
129+
}
130+
}
131+
132+
private computeSemanticTokenInfoForDeclaration(
133+
node: AstNode | undefined,
134+
additionalModifiers: SemanticTokenModifiers[] = [],
135+
): SemanticTokenInfo | void {
136+
/* c8 ignore start */
137+
if (!node) {
138+
return;
139+
}
140+
/* c8 ignore stop */
141+
142+
if (isSdsAnnotation(node)) {
143+
return {
144+
type: SemanticTokenTypes.decorator,
145+
modifier: additionalModifiers,
146+
};
147+
} else if (isSdsAttribute(node)) {
148+
const modifier = [SemanticTokenModifiers.readonly, ...additionalModifiers];
149+
if (node.isStatic) {
150+
modifier.push(SemanticTokenModifiers.static);
151+
}
152+
153+
return {
154+
type: SemanticTokenTypes.property,
155+
modifier,
156+
};
157+
} else if (isSdsClass(node)) {
158+
const isBuiltinClass = this.builtinClasses.isBuiltinClass(node);
159+
return {
160+
type: SemanticTokenTypes.class,
161+
modifier: isBuiltinClass
162+
? [SemanticTokenModifiers.defaultLibrary, ...additionalModifiers]
163+
: additionalModifiers,
164+
};
165+
} else if (isSdsEnum(node)) {
166+
return {
167+
type: SemanticTokenTypes.enum,
168+
modifier: additionalModifiers,
169+
};
170+
} else if (isSdsEnumVariant(node)) {
171+
return {
172+
type: SemanticTokenTypes.enumMember,
173+
modifier: additionalModifiers,
174+
};
175+
} else if (isSdsFunction(node)) {
176+
if (hasContainerOfType(node, isSdsClass)) {
177+
return {
178+
type: SemanticTokenTypes.method,
179+
modifier: node.isStatic
180+
? [SemanticTokenModifiers.static, ...additionalModifiers]
181+
: additionalModifiers,
182+
};
183+
} else {
184+
return {
185+
type: SemanticTokenTypes.function,
186+
modifier: additionalModifiers,
187+
};
188+
}
189+
} else if (isSdsModule(node)) {
190+
return {
191+
type: SemanticTokenTypes.namespace,
192+
modifier: additionalModifiers,
193+
};
194+
} else if (isSdsParameter(node)) {
195+
return {
196+
type: SemanticTokenTypes.parameter,
197+
modifier: additionalModifiers,
198+
};
199+
} else if (isSdsPipeline(node)) {
200+
return {
201+
type: SemanticTokenTypes.function,
202+
modifier: additionalModifiers,
203+
};
204+
} else if (isSdsPlaceholder(node)) {
205+
return {
206+
type: SemanticTokenTypes.variable,
207+
modifier: [SemanticTokenModifiers.readonly, ...additionalModifiers],
208+
};
209+
} else if (isSdsSegment(node)) {
210+
return {
211+
type: SemanticTokenTypes.function,
212+
modifier: additionalModifiers,
213+
};
214+
} else if (isSdsTypeParameter(node)) {
215+
return {
216+
type: SemanticTokenTypes.typeParameter,
217+
modifier: additionalModifiers,
218+
};
219+
}
220+
}
221+
}
222+
223+
interface SemanticTokenInfo {
224+
type: SemanticTokenTypes;
225+
modifier?: SemanticTokenModifiers | SemanticTokenModifiers[];
226+
}

src/language/safe-ds-module.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from 'langium';
1212
import { SafeDsGeneratedModule, SafeDsGeneratedSharedModule } from './generated/module.js';
1313
import { registerValidationChecks } from './validation/safe-ds-validator.js';
14-
import { SafeDsFormatter } from './formatting/safe-ds-formatter.js';
14+
import { SafeDsFormatter } from './lsp/safe-ds-formatter.js';
1515
import { SafeDsWorkspaceManager } from './workspace/safe-ds-workspace-manager.js';
1616
import { SafeDsScopeComputation } from './scoping/safe-ds-scope-computation.js';
1717
import { SafeDsScopeProvider } from './scoping/safe-ds-scope-provider.js';
@@ -23,6 +23,7 @@ import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js';
2323
import { SafeDsAnnotations } from './builtins/safe-ds-annotations.js';
2424
import { SafeDsClassHierarchy } from './typing/safe-ds-class-hierarchy.js';
2525
import { SafeDsPartialEvaluator } from './partialEvaluation/safe-ds-partial-evaluator.js';
26+
import { SafeDsSemanticTokenProvider } from './lsp/safe-ds-semantic-token-provider.js';
2627

2728
/**
2829
* Declaration of custom services - add your own service classes here.
@@ -71,6 +72,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
7172
},
7273
lsp: {
7374
Formatter: () => new SafeDsFormatter(),
75+
SemanticTokenProvider: (services) => new SafeDsSemanticTokenProvider(services),
7476
},
7577
parser: {
7678
ValueConverter: () => new SafeDsValueConverter(),

src/language/validation/builtins/deprecated.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { SafeDsServices } from '../../safe-ds-module.js';
1414
import { isRequiredParameter } from '../../helpers/nodeProperties.js';
1515
import { parameterCanBeAnnotated } from '../other/declarations/annotationCalls.js';
16+
import { DiagnosticTag } from 'vscode-languageserver-types';
1617

1718
export const CODE_DEPRECATED_ASSIGNED_RESULT = 'deprecated/assigned-result';
1819
export const CODE_DEPRECATED_CALLED_ANNOTATION = 'deprecated/called-annotation';
@@ -35,13 +36,14 @@ export const assigneeAssignedResultShouldNotBeDeprecated =
3536
accept('warning', `The assigned result '${assignedObject.name}' is deprecated.`, {
3637
node,
3738
code: CODE_DEPRECATED_ASSIGNED_RESULT,
39+
tags: [DiagnosticTag.Deprecated],
3840
});
3941
}
4042
};
4143

4244
export const annotationCallAnnotationShouldNotBeDeprecated =
4345
(services: SafeDsServices) => (node: SdsAnnotationCall, accept: ValidationAcceptor) => {
44-
const annotation = node.annotation.ref;
46+
const annotation = node.annotation?.ref;
4547
if (!annotation) {
4648
return;
4749
}
@@ -51,6 +53,7 @@ export const annotationCallAnnotationShouldNotBeDeprecated =
5153
node,
5254
property: 'annotation',
5355
code: CODE_DEPRECATED_CALLED_ANNOTATION,
56+
tags: [DiagnosticTag.Deprecated],
5457
});
5558
}
5659
};
@@ -66,6 +69,7 @@ export const argumentCorrespondingParameterShouldNotBeDeprecated =
6669
accept('warning', `The corresponding parameter '${parameter.name}' is deprecated.`, {
6770
node,
6871
code: CODE_DEPRECATED_CORRESPONDING_PARAMETER,
72+
tags: [DiagnosticTag.Deprecated],
6973
});
7074
}
7175
};
@@ -81,6 +85,7 @@ export const namedTypeDeclarationShouldNotBeDeprecated =
8185
accept('warning', `The referenced declaration '${declaration.name}' is deprecated.`, {
8286
node,
8387
code: CODE_DEPRECATED_REFERENCED_DECLARATION,
88+
tags: [DiagnosticTag.Deprecated],
8489
});
8590
}
8691
};
@@ -96,6 +101,7 @@ export const referenceTargetShouldNotBeDeprecated =
96101
accept('warning', `The referenced declaration '${target.name}' is deprecated.`, {
97102
node,
98103
code: CODE_DEPRECATED_REFERENCED_DECLARATION,
104+
tags: [DiagnosticTag.Deprecated],
99105
});
100106
}
101107
};

src/language/validation/builtins/experimental.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const assigneeAssignedResultShouldNotBeExperimental =
3737

3838
export const annotationCallAnnotationShouldNotBeExperimental =
3939
(services: SafeDsServices) => (node: SdsAnnotationCall, accept: ValidationAcceptor) => {
40-
const annotation = node.annotation.ref;
40+
const annotation = node.annotation?.ref;
4141
if (!annotation) {
4242
return;
4343
}

src/language/validation/builtins/repeatable.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const singleUseAnnotationsMustNotBeRepeated =
1414
});
1515

1616
for (const duplicate of duplicatesBy(callsOfSingleUseAnnotations, (it) => it.annotation?.ref)) {
17-
accept('error', `The annotation '${duplicate.annotation.$refText}' is not repeatable.`, {
17+
accept('error', `The annotation '${duplicate.annotation?.$refText}' is not repeatable.`, {
1818
node: duplicate,
1919
property: 'annotation',
2020
code: CODE_ANNOTATION_NOT_REPEATABLE,

0 commit comments

Comments
 (0)