Skip to content

Commit

Permalink
feat: semantic highlighting (#653)
Browse files Browse the repository at this point in the history
Closes #27

### Summary of Changes

Add semantic highlighting for declaration names and names used in
cross-references.

---------

Co-authored-by: megalinter-bot <[email protected]>
  • Loading branch information
lars-reimann and megalinter-bot authored Oct 20, 2023
1 parent c59856c commit fe8c602
Show file tree
Hide file tree
Showing 16 changed files with 466 additions and 13 deletions.
3 changes: 2 additions & 1 deletion docs/lexer/safe_ds_lexer/_safe_ds_lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
"Number",
"Int",
"Float",
"ListMap",
"List",
"Map",
"String",
)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
],
"configurationDefaults": {
"[safe-ds]": {
"editor.semanticHighlighting.enabled": true,
"editor.wordSeparators": "`~!@#$%^&*()-=+[]{}\\|;:'\",.<>/?»«",
"files.trimTrailingWhitespace": true
}
Expand Down
24 changes: 24 additions & 0 deletions src/language/builtins/safe-ds-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,34 @@ export class SafeDsClasses extends SafeDsModuleMembers<SdsClass> {
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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ fragment SdsSegmentFragment:
interface SdsAnnotationCallList extends SdsAnnotatedObject {}

interface SdsAnnotationCall extends SdsAbstractCall {
annotation: @SdsAnnotation
annotation?: @SdsAnnotation
}

SdsAnnotationCall returns SdsAnnotationCall:
Expand Down
File renamed without changes.
226 changes: 226 additions & 0 deletions src/language/lsp/safe-ds-semantic-token-provider.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
4 changes: 3 additions & 1 deletion src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -71,6 +72,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
},
lsp: {
Formatter: () => new SafeDsFormatter(),
SemanticTokenProvider: (services) => new SafeDsSemanticTokenProvider(services),
},
parser: {
ValueConverter: () => new SafeDsValueConverter(),
Expand Down
8 changes: 7 additions & 1 deletion src/language/validation/builtins/deprecated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
Expand All @@ -51,6 +53,7 @@ export const annotationCallAnnotationShouldNotBeDeprecated =
node,
property: 'annotation',
code: CODE_DEPRECATED_CALLED_ANNOTATION,
tags: [DiagnosticTag.Deprecated],
});
}
};
Expand All @@ -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],
});
}
};
Expand All @@ -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],
});
}
};
Expand All @@ -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],
});
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/language/validation/builtins/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/language/validation/builtins/repeatable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit fe8c602

Please sign in to comment.