Skip to content

Commit

Permalink
feat: signature help (#724)
Browse files Browse the repository at this point in the history
Closes #24

### Summary of Changes

Implement a signature help provider.
  • Loading branch information
lars-reimann authored Nov 5, 2023
1 parent a9eb3bb commit ed33676
Show file tree
Hide file tree
Showing 30 changed files with 466 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AstNode, DefaultCommentProvider, isAstNodeWithComment } from 'langium';
import { MarkupContent } from 'vscode-languageserver';
import {
isSdsBlockLambdaResult,
isSdsDeclaration,
Expand Down Expand Up @@ -31,3 +32,14 @@ export class SafeDsCommentProvider extends DefaultCommentProvider {
}
}
}

export const createMarkupContent = (documentation: string | undefined): MarkupContent | undefined => {
if (!documentation) {
return undefined;
}

return {
kind: 'markdown',
value: documentation,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export class SafeDsPythonGenerator {
const inputFile = `${inputPath.name}${inputPath.ext}`;

new TreeStreamImpl(trace, (r) => r.children ?? [], { includeRoot: true }).forEach((r) => {
if (!r.sourceRegion || !r.targetRegion || r.children?.[0].targetRegion.offset === r.targetRegion.offset) {
if (!r.sourceRegion || !r.targetRegion || r.children?.[0]?.targetRegion.offset === r.targetRegion.offset) {
return;
}
const sourceStart = r.sourceRegion.range?.start;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class SafeDsNodeMapper {

// A prior argument is named
for (let i = 0; i < argumentPosition; i++) {
if (isNamedArgument(args[i])) {
if (isNamedArgument(args[i]!)) {
return undefined;
}
}
Expand Down Expand Up @@ -233,7 +233,7 @@ export class SafeDsNodeMapper {

// A prior type argument is named
for (let i = 0; i < typeArgumentPosition; i++) {
if (isNamedTypeArgument(typeArguments[i])) {
if (isNamedTypeArgument(typeArguments[i]!)) {
return undefined;
}
}
Expand Down
12 changes: 6 additions & 6 deletions packages/safe-ds-lang/src/language/lsp/safe-ds-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import {
} from 'langium';
import * as ast from '../generated/ast.js';
import { getAnnotationCalls, getLiterals, getTypeArguments } from '../helpers/nodeProperties.js';
import { last } from '../../helpers/collectionUtils.js';
import noSpace = Formatting.noSpace;
import newLine = Formatting.newLine;
import newLines = Formatting.newLines;
import oneSpace = Formatting.oneSpace;
import indent = Formatting.indent;
import { last } from '../../helpers/collectionUtils.js';

const newLinesWithIndent = function (count: number, options?: FormattingActionOptions): FormattingAction {
return {
Expand Down Expand Up @@ -266,15 +266,15 @@ export class SafeDsFormatter extends AbstractFormatter {
} else {
const valueAnnotations = getAnnotationCalls(value);
if (valueAnnotations.length > 0) {
formatter.node(valueAnnotations[0]).prepend(newLines(2));
formatter.node(valueAnnotations[0]!).prepend(newLines(2));
} else {
formatter.node(value).prepend(newLines(2));
}
}
} else {
const valueAnnotations = getAnnotationCalls(value);
if (valueAnnotations.length > 0) {
formatter.node(valueAnnotations[0]).prepend(newLines(2));
formatter.node(valueAnnotations[0]!).prepend(newLines(2));
} else {
formatter.node(value).prepend(newLines(2));
}
Expand Down Expand Up @@ -852,7 +852,7 @@ export class SafeDsFormatter extends AbstractFormatter {
const literals = node.literals ?? [];

if (literals.length > 0) {
formatter.node(literals[0]).prepend(noSpace());
formatter.node(literals[0]!).prepend(noSpace());
formatter.nodes(...literals.slice(1)).prepend(oneSpace());
}

Expand Down Expand Up @@ -886,7 +886,7 @@ export class SafeDsFormatter extends AbstractFormatter {
closingBracket.prepend(newLine());
} else {
if (typeParameters.length > 0) {
formatter.node(typeParameters[0]).prepend(noSpace());
formatter.node(typeParameters[0]!).prepend(noSpace());
formatter.nodes(...typeParameters.slice(1)).prepend(oneSpace());
}
formatter.keywords(',').prepend(noSpace());
Expand All @@ -913,7 +913,7 @@ export class SafeDsFormatter extends AbstractFormatter {
const typeArguments = node.typeArguments ?? [];

if (typeArguments.length > 0) {
formatter.node(typeArguments[0]).prepend(noSpace());
formatter.node(typeArguments[0]!).prepend(noSpace());
formatter.nodes(...typeArguments.slice(1)).prepend(oneSpace());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { AbstractInlayHintProvider, AstNode, DocumentationProvider, InlayHintAcceptor } from 'langium';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { InlayHintKind, MarkupContent } from 'vscode-languageserver';
import { createMarkupContent } from '../documentation/safe-ds-comment-provider.js';
import { isSdsArgument, isSdsBlockLambdaResult, isSdsPlaceholder, isSdsYield } from '../generated/ast.js';
import { isPositionalArgument } from '../helpers/nodeProperties.js';
import { InlayHintKind, MarkupContent } from 'vscode-languageserver';
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { NamedType } from '../typing/model.js';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';

export class SafeDsInlayHintProvider extends AbstractInlayHintProvider {
private readonly documentationProvider: DocumentationProvider;
Expand Down Expand Up @@ -35,14 +36,14 @@ export class SafeDsInlayHintProvider extends AbstractInlayHintProvider {
position: cstNode.range.start,
label: `${parameter.name} = `,
kind: InlayHintKind.Parameter,
tooltip: createTooltip(this.documentationProvider.getDocumentation(parameter)),
tooltip: createMarkupContent(this.documentationProvider.getDocumentation(parameter)),
});
}
} else if (isSdsBlockLambdaResult(node) || isSdsPlaceholder(node) || isSdsYield(node)) {
const type = this.typeComputer.computeType(node);
let tooltip: MarkupContent | undefined = undefined;
if (type instanceof NamedType) {
tooltip = createTooltip(this.documentationProvider.getDocumentation(type.declaration));
tooltip = createMarkupContent(this.documentationProvider.getDocumentation(type.declaration));
}

acceptor({
Expand All @@ -54,14 +55,3 @@ export class SafeDsInlayHintProvider extends AbstractInlayHintProvider {
}
}
}

const createTooltip = (documentation: string | undefined): MarkupContent | undefined => {
if (!documentation) {
return undefined;
}

return {
kind: 'markdown',
value: documentation,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
type AstNode,
type DocumentationProvider,
findLeafNodeAtOffset,
findNodesForKeyword,
getContainerOfType,
isNamed,
type LangiumDocument,
type MaybePromise,
type SignatureHelpProvider,
} from 'langium';
import type {
CancellationToken,
SignatureHelp,
SignatureHelpOptions,
SignatureHelpParams,
} from 'vscode-languageserver';
import { createMarkupContent } from '../documentation/safe-ds-comment-provider.js';
import { isSdsAbstractCall, SdsCallable, SdsParameter } from '../generated/ast.js';
import { getParameters } from '../helpers/nodeProperties.js';
import { type SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import type { SafeDsServices } from '../safe-ds-module.js';
import { type SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import { CallableType, NamedType } from '../typing/model.js';

export class SafeDsSignatureHelpProvider implements SignatureHelpProvider {
private readonly documentationProvider: DocumentationProvider;
private readonly nodeMapper: SafeDsNodeMapper;
private readonly typeComputer: SafeDsTypeComputer;

constructor(services: SafeDsServices) {
this.documentationProvider = services.documentation.DocumentationProvider;
this.nodeMapper = services.helpers.NodeMapper;
this.typeComputer = services.types.TypeComputer;
}

provideSignatureHelp(
document: LangiumDocument,
params: SignatureHelpParams,
_cancelToken?: CancellationToken,
): MaybePromise<SignatureHelp | undefined> {
const rootCstNode = document.parseResult.value.$cstNode;
/* c8 ignore start */
if (!rootCstNode) {
return undefined;
}
/* c8 ignore stop */

const offset = document.textDocument.offsetAt(params.position);
const sourceCstNode = findLeafNodeAtOffset(rootCstNode, offset);
/* c8 ignore start */
if (!sourceCstNode) {
return undefined;
}
/* c8 ignore stop */

return this.getSignature(sourceCstNode.astNode, offset);
}

/**
* Returns the signature help for the given node at the given offset.
*/
private getSignature(node: AstNode, offset: number): MaybePromise<SignatureHelp | undefined> {
const containingAbstractCall = getContainerOfType(node, isSdsAbstractCall);
if (!containingAbstractCall) {
return undefined;
}

const callable = this.nodeMapper.callToCallable(containingAbstractCall);
if (!callable) {
return undefined;
}

const commas = findNodesForKeyword(containingAbstractCall.argumentList.$cstNode, ',');
const activeParameter = commas.findLastIndex((comma) => comma.offset < offset) + 1;

return {
signatures: [
{
label: this.getLabel(callable),
parameters: getParameters(callable).map(this.getParameterInformation),
documentation: createMarkupContent(this.documentationProvider.getDocumentation(callable)),
},
],
activeSignature: 0,
activeParameter,
};
}

private getLabel(callable: SdsCallable): string {
const type = this.typeComputer.computeType(callable);

if (type instanceof NamedType) {
return `${type.declaration.name}(${getParameters(callable)
.map((it) => this.getParameterLabel(it))
.join(', ')})`;
} else if (type instanceof CallableType && isNamed(callable)) {
return `${callable.name}${type}`;
} else {
return type.toString();
}
}

private getParameterInformation = (parameter: SdsParameter) => {
return {
label: this.getParameterLabel(parameter),
};
};

private getParameterLabel = (parameter: SdsParameter) => {
const type = this.typeComputer.computeType(parameter);
return `${parameter.name}: ${type}`;
};

/* c8 ignore start */
get signatureHelpOptions(): SignatureHelpOptions {
return {
triggerCharacters: ['('],
retriggerCharacters: [','],
};
}

/* c8 ignore stop */
}
Loading

0 comments on commit ed33676

Please sign in to comment.