Skip to content

feat: establish language server service for the graphical editor #1315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
40 changes: 39 additions & 1 deletion packages/safe-ds-lang/src/language/communication/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { MessageDirection, NotificationType0, RequestType0 } from 'vscode-languageserver';
import { NotificationType } from 'vscode-languageserver-protocol';
import { NotificationType, RequestType } from 'vscode-languageserver-protocol';
import { UUID } from 'node:crypto';
import { Buildin, Collection } from '../graphical-editor/global.js';
import { Uri } from 'vscode';

export namespace InstallRunnerNotification {
export const method = 'runner/install' as const;
Expand Down Expand Up @@ -91,3 +93,39 @@ export namespace IsRunnerReadyRequest {
export const messageDirection = MessageDirection.clientToServer;
export const type = new RequestType0(method);
}

export namespace GraphicalEditorSyncEventNotification {
export const method = 'graphical-editor/sync-event' as const;
export const messageDirection = MessageDirection.serverToClient;
export const type = new NotificationType<Collection>(method);
}

export namespace GraphicalEditorOpenSyncChannelRequest {
export const method = 'graphical-editor/openSyncChannel' as const;
export const messageDirection = MessageDirection.clientToServer;
export const type = new RequestType<Uri, void, void>(method);
}

export namespace GraphicalEditorCloseSyncChannelRequest {
export const method = 'graphical-editor/closeSyncChannel' as const;
export const messageDirection = MessageDirection.clientToServer;
export const type = new RequestType<Uri, void, void>(method);
}

export namespace GraphicalEditorGetDocumentationRequest {
export const method = 'graphical-editor/getDocumentation' as const;
export const messageDirection = MessageDirection.clientToServer;
export const type = new RequestType<{ uri: Uri; uniquePath: string }, string | undefined, void>(method);
}

export namespace GraphicalEditorGetBuildinsRequest {
export const method = 'graphical-editor/getBuildins' as const;
export const messageDirection = MessageDirection.clientToServer;
export const type = new RequestType<void, Buildin[], void>(method);
}

export namespace GraphicalEditorParseDocumentRequest {
export const method = 'graphical-editor/parseDocument' as const;
export const messageDirection = MessageDirection.clientToServer;
export const type = new RequestType<Uri, Collection, void>(method);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { isSdsLiteral, SdsArgument } from '../../generated/ast.js';
import { Call } from './call.js';
import { Placeholder } from './placeholder.js';
import { Expression, GenericExpression } from './expression.js';
import { Parameter } from './parameter.js';
import { Parser } from './parser.js';
import { CustomError } from '../types.js';

export class Argument {
constructor(
public readonly text: string,
public readonly reference: GenericExpression | Call | Placeholder | Parameter | undefined,
public readonly parameterName?: string,
) {}

public static parse(node: SdsArgument, parser: Parser) {
if (!node.value.$cstNode) return parser.pushError('CstNode missing', node.value);
const text = node.value.$cstNode.text;

let expression;
if (!isSdsLiteral(node.value)) expression = Expression.parse(node.value, parser);
if (expression instanceof CustomError) return expression;

if (node.parameter && !node.parameter.ref) return parser.pushError('Missing Parameterreference', node);
const parameterName = node.parameter?.ref?.name;

return new Argument(text, expression, parameterName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import {
SdsCall,
SdsClass,
SdsExpression,
SdsFunction,
SdsMemberAccess,
SdsPlaceholder,
SdsReference,
SdsSegment,
isSdsCall,
isSdsClass,
isSdsFunction,
isSdsMemberAccess,
isSdsPlaceholder,
isSdsReference,
isSdsSegment,
} from '../../generated/ast.js';
import { Argument } from './argument.js';
import { Edge, Port } from './edge.js';
import { GenericExpression } from './expression.js';
import { Parameter } from './parameter.js';
import { Placeholder } from './placeholder.js';
import { Result } from './result.js';
import { filterErrors } from './utils.js';
import { Parser } from './parser.js';
import { CustomError } from '../types.js';

export class Call {
private constructor(
public readonly id: number,
public readonly name: string,
public readonly self: string | undefined,
public readonly parameterList: Parameter[],
public readonly resultList: Result[],
public readonly category: string,
public readonly uniquePath: string,
) {}

public static parse(node: SdsCall, parser: Parser): Call | CustomError {
const id = parser.getNewId();

if (!isValidCallReceiver(node.receiver)) {
return parser.pushError(`Invalid Call receiver: ${debugInvalidCallReceiver(node.receiver)}`, node.receiver);
}

let name = '';
let self: string | undefined = undefined;
let category = '';
let argumentList: Argument[] = [];
let parameterList: Parameter[] = [];
let resultList: Result[] = [];

argumentList = filterErrors(node.argumentList.arguments.map((argument) => Argument.parse(argument, parser)));

if (isSdsMemberAccess(node.receiver)) {
const tmp = Call.parseSelf(node.receiver, id, parser);
if (tmp instanceof CustomError) return tmp;
self = tmp;

const functionDeclaration = node.receiver.member.target.ref;
name = functionDeclaration.name;
category = parser.getCategory(functionDeclaration)?.name ?? '';

resultList = filterErrors(
(functionDeclaration.resultList?.results ?? []).map((result) => Result.parse(result, parser)),
);
parameterList = filterErrors(
(functionDeclaration.parameterList?.parameters ?? []).map((parameter) =>
Parameter.parse(parameter, parser),
),
);
}

if (isSdsReference(node.receiver) && isSdsClass(node.receiver.target.ref)) {
const classDeclaration = node.receiver.target.ref;

name = 'new';
self = classDeclaration.name;
category = 'Modeling';

if (!classDeclaration.parameterList)
return parser.pushError('Missing constructor parameters', classDeclaration);
parameterList = filterErrors(
classDeclaration.parameterList.parameters.map((parameter) => Parameter.parse(parameter, parser)),
);
resultList = [new Result('new', classDeclaration.name)];
}

if (isSdsReference(node.receiver) && isSdsSegment(node.receiver.target.ref)) {
const segmentDeclaration = node.receiver.target.ref;

self = '';
name = segmentDeclaration.name;
category = 'Segment';

resultList = filterErrors(
(segmentDeclaration.resultList?.results ?? []).map((result) => Result.parse(result, parser)),
);
parameterList = filterErrors(
(segmentDeclaration.parameterList?.parameters ?? []).map((parameter) =>
Parameter.parse(parameter, parser),

Check warning on line 101 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L101

Added line #L101 was not covered by tests
),
);
}

const parameterListCompleted = matchArgumentsToParameter(parameterList, argumentList, node, id, parser);
if (parameterListCompleted instanceof CustomError) return parameterListCompleted;

const call = new Call(id, name, self, parameterListCompleted, resultList, category, parser.getUniquePath(node));
parser.graph.callList.push(call);
return call;
}

private static parseSelf(node: CallReceiver, id: number, parser: Parser) {
if (isSdsMemberAccess(node)) {
if (isSdsCall(node.receiver)) {
const call = Call.parse(node.receiver, parser);
if (call instanceof CustomError) return call;

Check warning on line 118 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L117-L118

Added lines #L117 - L118 were not covered by tests

if (call.resultList.length > 1) return parser.pushError('To many result', node.receiver);
if (call.resultList.length < 1) return parser.pushError('Missing result', node.receiver);

Check warning on line 121 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L120-L121

Added lines #L120 - L121 were not covered by tests

Edge.create(Port.fromResult(call.resultList[0]!, call.id), Port.fromName(id, 'self'), parser);

Check warning on line 123 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L123

Added line #L123 was not covered by tests
} else if (isSdsReference(node.receiver)) {
const receiver = node.receiver.target.ref;

if (isSdsClass(receiver)) {
return receiver.name;
} else if (isSdsPlaceholder(receiver)) {
const placeholder = Placeholder.parse(receiver, parser);
Edge.create(Port.fromPlaceholder(placeholder, false), Port.fromName(id, 'self'), parser);
}
}
}
return '';
}
}

const matchArgumentsToParameter = (
parameterList: Parameter[],
argumentList: Argument[],
callNode: SdsCall,
id: number,
parser: Parser,
): Parameter[] | CustomError => {
for (const [i, parameter] of parameterList.entries()) {
const argumentIndexMatched = argumentList[i];
if (argumentIndexMatched instanceof CustomError) return argumentIndexMatched;

const argumentNameMatched = argumentList.find(
(argument) => !(argument instanceof CustomError) && argument.parameterName === parameter.name,
) as Argument | undefined;

if (argumentIndexMatched && argumentNameMatched && argumentIndexMatched !== argumentNameMatched)
return parser.pushError(`To many matches for ${parameter.name}`, callNode.argumentList);
const argument = argumentIndexMatched ?? argumentNameMatched;

if (argument) {
parameter.argumentText = argument.text;
if (argument.reference instanceof Call) {
const call = argument.reference;
if (call.resultList.length !== 1) return parser.pushError('Type missmatch', callNode.argumentList);
Edge.create(Port.fromResult(call.resultList[0]!, call.id), Port.fromParameter(parameter, id), parser);
}

Check warning on line 164 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L161-L164

Added lines #L161 - L164 were not covered by tests
if (argument.reference instanceof GenericExpression) {
const experession = argument.reference;
Edge.create(Port.fromGenericExpression(experession, false), Port.fromParameter(parameter, id), parser);
}
if (argument.reference instanceof Placeholder) {
const placeholder = argument.reference;
Edge.create(Port.fromPlaceholder(placeholder, false), Port.fromParameter(parameter, id), parser);
}

Check warning on line 172 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L170-L172

Added lines #L170 - L172 were not covered by tests
if (argument.reference instanceof Parameter) {
const segmentParameter = argument.reference;
Edge.create(Port.fromParameter(segmentParameter, -1), Port.fromParameter(parameter, id), parser);
}
continue;
}

if (!argument && parameter.defaultValue) {
continue;
}

if (!argument && !parameter.defaultValue) {
return parser.pushError(`Missing Argument for ${parameter.name}`, callNode);
}

Check warning on line 186 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L185-L186

Added lines #L185 - L186 were not covered by tests
}
return parameterList;
};

type CallReceiver =
| (SdsReference & { target: { ref: SdsClass | SdsSegment } })
| (SdsMemberAccess & {
member: {
target: { ref: SdsFunction };
};
receiver: SdsCall | { target: { ref: SdsPlaceholder | SdsClass } };
});

const isValidCallReceiver = (receiver: SdsExpression): receiver is CallReceiver => {
/* eslint-disable no-implicit-coercion */
return (
(isSdsMemberAccess(receiver) &&
!!receiver.member &&
!!receiver.member.target.ref &&
isSdsFunction(receiver.member.target.ref) &&
((isSdsReference(receiver.receiver) &&
(isSdsClass(receiver.receiver.target.ref) || isSdsPlaceholder(receiver.receiver.target.ref))) ||
isSdsCall(receiver.receiver))) ||
(isSdsReference(receiver) && (isSdsClass(receiver.target.ref) || isSdsSegment(receiver.target.ref)))
);
};

const debugInvalidCallReceiver = (receiver: SdsExpression): string => {
/* eslint-disable no-implicit-coercion */
if (isSdsMemberAccess(receiver)) {
if (!receiver.member) return 'MemberAccess: Missing member';
if (!receiver.member.target.ref) return 'MemberAccess: Missing member declaration';
if (!isSdsFunction(receiver.member.target.ref)) return 'MemberAccess: Member is not a function';
if (!isSdsCall(receiver.receiver) && !isSdsReference(receiver.receiver))
return `MemberAccess: Receiver is not a Reference or Call but - ${receiver.receiver.$type}`;
if (
isSdsReference(receiver.receiver) &&
!isSdsClass(receiver.receiver.target.ref) &&
isSdsReference(receiver.receiver) &&
!isSdsPlaceholder(receiver.receiver.target.ref)

Check warning on line 226 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L217-L226

Added lines #L217 - L226 were not covered by tests
)
return 'MemberAccess: Reference Receiver is not Class of Placeholder';
}

Check warning on line 229 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts#L228-L229

Added lines #L228 - L229 were not covered by tests
if (isSdsReference(receiver)) {
if (!isSdsClass(receiver.target.ref) && !isSdsSegment(receiver.target.ref))
return 'Reference: Not a class or segment';
}

return receiver.$type;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { GenericExpression } from './expression.js';
import { Parameter } from './parameter.js';
import { Parser } from './parser.js';
import { Placeholder } from './placeholder.js';
import { Result } from './result.js';
import { SegmentGroupId } from './segment.js';

export class Edge {
public constructor(
public readonly from: Port,
public readonly to: Port,
) {}

public static create(from: Port, to: Port, parser: Parser) {
parser.graph.edgeList.push(new Edge(from, to));
}
}

export class Port {
private constructor(
public readonly nodeId: string,
public readonly portIdentifier: string,
) {}

public static fromName = (nodeId: number, name: string): Port => {
return new Port(nodeId.toString(), name);
};

public static fromPlaceholder = (placeholder: Placeholder, input: boolean): Port => {
return new Port(placeholder.name, input ? 'target' : 'source');
};

public static fromResult = (result: Result, nodeId: number): Port => {
return new Port(nodeId.toString(), result.name);
};

public static fromParameter = (parameter: Parameter, nodeId: number): Port => {
return new Port(nodeId.toString(), parameter.name);
};

public static fromGenericExpression(node: GenericExpression, input: boolean) {
return new Port(node.id.toString(), input ? 'target' : 'source');
}

public static fromAssignee = (node: Placeholder | Result, input: boolean): Port => {
if (node instanceof Placeholder) {
return new Port(node.name, input ? 'target' : 'source');
}
return new Port(SegmentGroupId.toString(), node.name);
};

public static isPortList(object: any): object is Port[] {
return Array.isArray(object) && object.every((element) => element instanceof Port);
}

Check warning on line 54 in packages/safe-ds-lang/src/language/graphical-editor/ast-parser/edge.ts

View check run for this annotation

Codecov / codecov/patch

packages/safe-ds-lang/src/language/graphical-editor/ast-parser/edge.ts#L53-L54

Added lines #L53 - L54 were not covered by tests
}
Loading