Skip to content

Commit

Permalink
feat: document symbol provider (#659)
Browse files Browse the repository at this point in the history
### Summary of Changes

Implement a document symbol provider. Compared to the default provider,
it sets proper symbol kinds, adds details for functions and segments to
show their signature, and marks symbols as deprecated.

---------

Co-authored-by: megalinter-bot <[email protected]>
  • Loading branch information
lars-reimann and megalinter-bot authored Oct 21, 2023
1 parent 9ba9d20 commit fe0c8d5
Show file tree
Hide file tree
Showing 84 changed files with 847 additions and 399 deletions.
446 changes: 222 additions & 224 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 8 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
}
],
"engines": {
"vscode": "^1.82.0"
"vscode": "^1.83.0"
},
"contributes": {
"languages": [
Expand Down Expand Up @@ -103,25 +103,23 @@
"dependencies": {
"chalk": "^5.3.0",
"chevrotain": "^11.0.3",
"commander": "^11.0.0",
"commander": "^11.1.0",
"glob": "^10.3.10",
"langium": "^2.0.2",
"radash": "^11.0.0",
"true-myth": "^7.1.0",
"vscode-languageclient": "^9.0.1",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-types": "^3.17.5",
"vscode-uri": "^3.0.7"
"vscode-languageserver": "^9.0.1"
},
"devDependencies": {
"@lars-reimann/eslint-config": "^5.1.3",
"@lars-reimann/eslint-config": "^5.1.4",
"@lars-reimann/prettier-config": "^5.0.0",
"@types/node": "^18.18.1",
"@types/vscode": "^1.82.0",
"@types/node": "^18.18.6",
"@types/vscode": "^1.83.1",
"@vitest/coverage-v8": "^0.34.6",
"@vitest/ui": "^0.34.6",
"concurrently": "^8.2.1",
"esbuild": "^0.19.4",
"concurrently": "^8.2.2",
"esbuild": "^0.19.5",
"esbuild-plugin-copy": "^2.1.1",
"langium-cli": "^2.0.1",
"typescript": "^5.2.2",
Expand Down
89 changes: 89 additions & 0 deletions src/language/lsp/safe-ds-document-symbol-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { AstNode, DefaultDocumentSymbolProvider, LangiumDocument } from 'langium';
import { DocumentSymbol, SymbolTag } from 'vscode-languageserver';
import { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js';
import {
isSdsAnnotatedObject,
isSdsAnnotation,
isSdsAttribute,
isSdsClass,
isSdsEnumVariant,
isSdsFunction,
isSdsPipeline,
isSdsSegment,
} from '../generated/ast.js';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';

export class SafeDsDocumentSymbolProvider extends DefaultDocumentSymbolProvider {
private readonly builtinAnnotations: SafeDsAnnotations;
private readonly typeComputer: SafeDsTypeComputer;

constructor(services: SafeDsServices) {
super(services);

this.builtinAnnotations = services.builtins.Annotations;
this.typeComputer = services.types.TypeComputer;
}

protected override getSymbol(document: LangiumDocument, node: AstNode): DocumentSymbol[] {
const cstNode = node.$cstNode;
const nameNode = this.nameProvider.getNameNode(node);
if (nameNode && cstNode) {
const name = this.nameProvider.getName(node);
return [
{
name: name ?? nameNode.text,
kind: this.nodeKindProvider.getSymbolKind(node),
tags: this.getTags(node),
detail: this.getDetails(node),
range: cstNode.range,
selectionRange: nameNode.range,
children: this.getChildSymbols(document, node),
},
];
} else {
return this.getChildSymbols(document, node) || [];
}
}

protected override getChildSymbols(document: LangiumDocument, node: AstNode): DocumentSymbol[] | undefined {
if (this.isLeaf(node)) {
return undefined;
} else if (isSdsClass(node)) {
if (node.body) {
return super.getChildSymbols(document, node.body);
} else {
return undefined;
}
} else {
return super.getChildSymbols(document, node);
}
}

private getDetails(node: AstNode): string | undefined {
if (isSdsFunction(node) || isSdsSegment(node)) {
const type = this.typeComputer.computeType(node);
return type?.toString();
}
return undefined;
}

private getTags(node: AstNode): SymbolTag[] | undefined {
if (isSdsAnnotatedObject(node) && this.builtinAnnotations.isDeprecated(node)) {
return [SymbolTag.Deprecated];
} else {
return undefined;
}
}

private isLeaf(node: AstNode): boolean {
return (
isSdsAnnotation(node) ||
isSdsAttribute(node) ||
isSdsEnumVariant(node) ||
isSdsFunction(node) ||
isSdsPipeline(node) ||
isSdsSegment(node)
);
}
}
93 changes: 93 additions & 0 deletions src/language/lsp/safe-ds-node-kind-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { AstNode, AstNodeDescription, hasContainerOfType, isAstNode, NodeKindProvider } from 'langium';
import { CompletionItemKind, SymbolKind } from 'vscode-languageserver';
import {
isSdsClass,
isSdsFunction,
SdsAnnotation,
SdsAttribute,
SdsBlockLambdaResult,
SdsClass,
SdsEnum,
SdsEnumVariant,
SdsFunction,
SdsModule,
SdsParameter,
SdsPipeline,
SdsPlaceholder,
SdsResult,
SdsSegment,
SdsTypeParameter,
} from '../generated/ast.js';

export class SafeDsNodeKindProvider implements NodeKindProvider {
getSymbolKind(nodeOrDescription: AstNode | AstNodeDescription): SymbolKind {
// The WorkspaceSymbolProvider only passes descriptions, where the node might be undefined
const node = this.getNode(nodeOrDescription);
if (isSdsFunction(node) && hasContainerOfType(node, isSdsClass)) {
return SymbolKind.Method;
}

const type = this.getNodeType(nodeOrDescription);
switch (type) {
case SdsAnnotation:
return SymbolKind.Interface;
case SdsAttribute:
return SymbolKind.Property;
/* c8 ignore next 2 */
case SdsBlockLambdaResult:
return SymbolKind.Variable;
case SdsClass:
return SymbolKind.Class;
case SdsEnum:
return SymbolKind.Enum;
case SdsEnumVariant:
return SymbolKind.EnumMember;
case SdsFunction:
return SymbolKind.Function;
case SdsModule:
return SymbolKind.Package;
/* c8 ignore next 2 */
case SdsParameter:
return SymbolKind.Variable;
case SdsPipeline:
return SymbolKind.Function;
/* c8 ignore next 2 */
case SdsPlaceholder:
return SymbolKind.Variable;
/* c8 ignore next 2 */
case SdsResult:
return SymbolKind.Variable;
case SdsSegment:
return SymbolKind.Function;
/* c8 ignore next 2 */
case SdsTypeParameter:
return SymbolKind.TypeParameter;
/* c8 ignore next 2 */
default:
return SymbolKind.Null;
}
}

/* c8 ignore start */
getCompletionItemKind(_nodeOrDescription: AstNode | AstNodeDescription) {
return CompletionItemKind.Reference;
}

/* c8 ignore stop */

private getNode(nodeOrDescription: AstNode | AstNodeDescription): AstNode | undefined {
if (isAstNode(nodeOrDescription)) {
return nodeOrDescription;
} /* c8 ignore start */ else {
return nodeOrDescription.node;
} /* c8 ignore stop */
}

private getNodeType(nodeOrDescription: AstNode | AstNodeDescription): string {
if (isAstNode(nodeOrDescription)) {
return nodeOrDescription.$type;
} /* c8 ignore start */ else {
return nodeOrDescription.type;
} /* c8 ignore stop */
}
}
4 changes: 2 additions & 2 deletions src/language/lsp/safe-ds-semantic-token-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {
AbstractSemanticTokenProvider,
AllSemanticTokenTypes,
AstNode,
DefaultSemanticTokenOptions,
hasContainerOfType,
SemanticTokenAcceptor,
DefaultSemanticTokenOptions,
} from 'langium';
import {
isSdsAnnotation,
Expand All @@ -29,7 +29,7 @@ import {
isSdsTypeParameter,
isSdsTypeParameterConstraint,
} from '../generated/ast.js';
import { SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver-types';
import { SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver';
import { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';

Expand Down
2 changes: 1 addition & 1 deletion src/language/partialEvaluation/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ class UnknownEvaluatedNodeClass extends EvaluatedNode {
}

override toString(): string {
return '$unknown';
return '?';
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { SafeDsPartialEvaluator } from './partialEvaluation/safe-ds-partial-eval
import { SafeDsSemanticTokenProvider } from './lsp/safe-ds-semantic-token-provider.js';
import { SafeDsTypeChecker } from './typing/safe-ds-type-checker.js';
import { SafeDsCoreTypes } from './typing/safe-ds-core-types.js';
import { SafeDsNodeKindProvider } from './lsp/safe-ds-node-kind-provider.js';
import { SafeDsDocumentSymbolProvider } from './lsp/safe-ds-document-symbol-provider.js';

/**
* Declaration of custom services - add your own service classes here.
Expand Down Expand Up @@ -75,6 +77,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
NodeMapper: (services) => new SafeDsNodeMapper(services),
},
lsp: {
DocumentSymbolProvider: (services) => new SafeDsDocumentSymbolProvider(services),
Formatter: () => new SafeDsFormatter(),
SemanticTokenProvider: (services) => new SafeDsSemanticTokenProvider(services),
},
Expand All @@ -99,6 +102,9 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
export type SafeDsSharedServices = LangiumSharedServices;

export const SafeDsSharedModule: Module<SafeDsSharedServices, DeepPartial<SafeDsSharedServices>> = {
lsp: {
NodeKindProvider: () => new SafeDsNodeKindProvider(),
},
workspace: {
WorkspaceManager: (services) => new SafeDsWorkspaceManager(services),
},
Expand Down
2 changes: 1 addition & 1 deletion src/language/typing/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ class UnknownTypeClass extends Type {
}

toString(): string {
return '$Unknown';
return '?';
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/language/validation/builtins/deprecated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +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';
import { DiagnosticTag } from 'vscode-languageserver';

export const CODE_DEPRECATED_ASSIGNED_RESULT = 'deprecated/assigned-result';
export const CODE_DEPRECATED_CALLED_ANNOTATION = 'deprecated/called-annotation';
Expand Down
2 changes: 1 addition & 1 deletion src/language/validation/other/declarations/placeholders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +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';
import { DiagnosticTag } from 'vscode-languageserver';

export const CODE_PLACEHOLDER_ALIAS = 'placeholder/alias';
export const CODE_PLACEHOLDER_UNUSED = 'placeholder/unused';
Expand Down
2 changes: 1 addition & 1 deletion src/language/validation/other/declarations/segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +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';
import { DiagnosticTag } from 'vscode-languageserver';

export const CODE_SEGMENT_DUPLICATE_YIELD = 'segment/duplicate-yield';
export const CODE_SEGMENT_UNASSIGNED_RESULT = 'segment/unassigned-result';
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parseHelper } from 'langium/test';
import { LangiumServices, URI } from 'langium';
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types';
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
import { TestDescriptionError } from './testDescription.js';

let nextId = 0;
Expand Down
2 changes: 1 addition & 1 deletion tests/language/builtins/builtinFilesCorrectness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { listBuiltinFiles } from '../../../src/language/builtins/fileFinder.js';
import { uriToShortenedResourceName } from '../../../src/helpers/resources.js';
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
import { NodeFileSystem } from 'langium/node';
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types';
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
import { isEmpty } from 'radash';
import { URI } from 'langium';
import { locationToString } from '../../helpers/location.js';
Expand Down
2 changes: 1 addition & 1 deletion tests/language/lsp/formatting/creator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { listTestSafeDsFiles, uriToShortenedTestResourceName } from '../../../helpers/testResources.js';
import fs from 'fs';
import { Diagnostic } from 'vscode-languageserver-types';
import { Diagnostic } from 'vscode-languageserver';
import { createSafeDsServices } from '../../../../src/language/safe-ds-module.js';
import { EmptyFileSystem, URI } from 'langium';
import { getSyntaxErrors } from '../../../helpers/diagnostics.js';
Expand Down
Loading

0 comments on commit fe0c8d5

Please sign in to comment.