diff --git a/.vscode/global.code-snippets b/.vscode/global.code-snippets new file mode 100644 index 000000000..de018742b --- /dev/null +++ b/.vscode/global.code-snippets @@ -0,0 +1,15 @@ +{ + "Copyright Header": { + "prefix": "header", + "body": [ + "/******************************************************************************", + " * Copyright ${CURRENT_YEAR} TypeFox GmbH", + " * This program and the accompanying materials are made available under the", + " * terms of the MIT License, which is available in the project root.", + " ******************************************************************************/", + "", + "$0" + ], + "description": "Insert TypeFox copyright header" + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 27a6db0f0..4de3d66e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,5 +22,26 @@ }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" - } + }, + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#31afb4", + "activityBar.background": "#31afb4", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#af30ab", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#31afb4", + "statusBar.background": "#26888c", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#31afb4", + "statusBarItem.remoteBackground": "#26888c", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#26888c", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#26888c99", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.remoteColor": "#26888c", + "peacock.color": "#26888c", } diff --git a/examples/arithmetics/example/example.calc b/examples/arithmetics/example/example.calc index e2af2f09f..40e735d8d 100644 --- a/examples/arithmetics/example/example.calc +++ b/examples/arithmetics/example/example.calc @@ -3,11 +3,14 @@ Module basicMath def a: 5; def b: 3; def c: a + b; // 8 -def d: (a ^ b); // 164 +def d: (a^b); // 164 def root(x, y): x^(1/y); +def empty(_): + 0; + def sqrt(x): root(x, 2); @@ -17,4 +20,5 @@ b % 2; // 1 // This language is case-insensitive regarding symbol names Root(D, 3); // 32 Root(64, 3); // 4 -Sqrt(81); // 9 \ No newline at end of file +Sqrt(81); // 9 +empty(a + b); // 0 \ No newline at end of file diff --git a/examples/arithmetics/example/infix-rule-bug.calc b/examples/arithmetics/example/infix-rule-bug.calc new file mode 100644 index 000000000..ab89d73d5 --- /dev/null +++ b/examples/arithmetics/example/infix-rule-bug.calc @@ -0,0 +1,7 @@ +module Single + +def a: 5; +def b: 99; + +def adaduf(x, y, z): + x+y+z; \ No newline at end of file diff --git a/examples/arithmetics/example/poor example.calc b/examples/arithmetics/example/poor example.calc new file mode 100644 index 000000000..c3aaf0791 --- /dev/null +++ b/examples/arithmetics/example/poor example.calc @@ -0,0 +1,21 @@ +Module basicMath +def a: 5; +def b: 3; +def c: a + b; // 8 +def d: (a ^ b); // 164 + +def root(x, y): + x ^( 1 / y); + +def sqrt(x): + root(x, 2); + +def empty(_): + a +b+ c; +2 * c; // 16 +b % 2; // 1 + +// This language is case-insensitive regarding symbol names +Root(D, 3); // 32 +Root(64, 3); // 4 +Sqrt(81); // 9 \ No newline at end of file diff --git a/examples/arithmetics/src/language-server/arithmetics-evaluator.ts b/examples/arithmetics/src/language-server/arithmetics-evaluator.ts index 5078bb025..06ffa7d73 100644 --- a/examples/arithmetics/src/language-server/arithmetics-evaluator.ts +++ b/examples/arithmetics/src/language-server/arithmetics-evaluator.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ import type { AbstractDefinition, Definition, Evaluation, Expression, Module, Statement } from './generated/ast.js'; -import { isBinaryExpression, isDefinition, isEvaluation, isFunctionCall, isNumberLiteral } from './generated/ast.js'; +import { isBinaryExpression, isDefinition, isEvaluation, isFunctionCall, isNestedExpression, isNumberLiteral } from './generated/ast.js'; import { applyOp } from './arithmetics-util.js'; export function interpretEvaluations(module: Module): Map { @@ -79,6 +79,9 @@ export function evalExpression(expr: Expression, ctx?: InterpreterContext): numb } return evalExpression(valueOrDef.expr, {module: ctx.module, context: localContext, result: ctx.result}); } + if (isNestedExpression(expr)) { + return evalExpression(expr.value, ctx); + } throw new Error('Impossible type of Expression.'); } diff --git a/examples/arithmetics/src/language-server/arithmetics-formatter.ts b/examples/arithmetics/src/language-server/arithmetics-formatter.ts new file mode 100644 index 000000000..ba286043b --- /dev/null +++ b/examples/arithmetics/src/language-server/arithmetics-formatter.ts @@ -0,0 +1,87 @@ +/****************************************************************************** + * Copyright 2025 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import type { AstNode } from 'langium'; +import { AbstractFormatter, Formatting, type NodeFormatter } from 'langium/lsp'; +import * as ast from './generated/ast.js'; + +export class ArithmeticsFormatter extends AbstractFormatter { + protected override format(node: AstNode): void { + if (ast.isModule(node)) { + const formatter = this.getNodeFormatter(node); + // Format module declaration: "module" keyword followed by space, then name + formatter.keyword('module').append(Formatting.oneSpace()); + formatter.property('name').append(Formatting.newLine()); + // All statements should be aligned to the root (no additional indentation) + const statements = formatter.nodes(...node.statements); + statements.prepend(Formatting.noIndent()); + } else if (ast.isDefinition(node)) { + const formatter = this.getNodeFormatter(node); + formatter.keyword('def').append(Formatting.oneSpace()); + formatter.keyword(':').prepend(Formatting.noSpace()); + if (node.args.length > 0) { + // Format Definition of a function + formatParameters(formatter); + formatter.property('expr').prepend(Formatting.indent()); + } else { + // Format Definition of a constant + formatter.property('expr').prepend(Formatting.oneSpace()); + } + + } else if (ast.isFunctionCall(node)) { + const formatter = this.getNodeFormatter(node); + formatParameters(formatter); + } else if (ast.isNestedExpression(node)) { + const formatter = this.getNodeFormatter(node); + // Keep parentheses tight with no spaces inside (but don't restrict spaces outside) + formatter.keyword('(').append(Formatting.noSpace()); + formatter.keyword(')').prepend(Formatting.noSpace()); + } else if (ast.isBinaryExpression(node)) { + // const formatter = this.getNodeFormatter(node); + // FIXME: Infix rules assign incorrect CST nodes to left/right in some cases. + /* Example: + * ```calc + * module Single + * + * def adaduf(x, y, z): + * x+y+z; + * ``` + * The `+` between `x` and `y` is incorrectly represented with left CST text =`x+y+z`, right CST text = `z` + * AST Nodes, however, seems to be attached correctly: on the left CST node we have a BinaryExpression with left=`x`, right=`y` + * + * For now, we don't apply spacing within BinaryExpressions at all, else it not only gets partial formatting, + * but even unexpectedly affects rules *outside* of the BinaryExpression CST! + */ + // operators cannot be formatted neither as keywords nor as properties + // left/right property cannot be formatted either + // formatter.node(node.left).append(getOperatorSpacing(node.operator)); + // formatter.node(node.right).prepend(getOperatorSpacing(node.operator)); + } + + // No space around semicolons in all cases + const formatter = this.getNodeFormatter(node); + formatter.keyword(';').surround(Formatting.noSpace()); + } +} + +function formatParameters(formatter: NodeFormatter): void { + formatter.keywords('(', ')').surround(Formatting.noSpace()); + formatter.keywords(',') + .prepend(Formatting.noSpace()).append(Formatting.oneSpace({ allowMore: false })); +} + +// function getOperatorSpacing(operator: ast.BinaryExpression['operator']): FormattingAction { +// switch (operator) { +// case '+': +// case '-': +// case '%': +// return Formatting.oneSpace({ allowMore: false }); +// case '*': +// case '/': +// case '^': +// return Formatting.noSpace(); +// } +// } diff --git a/examples/arithmetics/src/language-server/arithmetics-module.ts b/examples/arithmetics/src/language-server/arithmetics-module.ts index cc29473dc..1402c2eeb 100644 --- a/examples/arithmetics/src/language-server/arithmetics-module.ts +++ b/examples/arithmetics/src/language-server/arithmetics-module.ts @@ -6,6 +6,7 @@ import { type Module, inject } from 'langium'; import { createDefaultModule, createDefaultSharedModule, type DefaultSharedModuleContext, type LangiumServices, type LangiumSharedServices, type PartialLangiumServices } from 'langium/lsp'; +import { ArithmeticsFormatter } from './arithmetics-formatter.js'; import { ArithmeticsScopeProvider } from './arithmetics-scope-provider.js'; import { ArithmeticsValidator, registerValidationChecks } from './arithmetics-validator.js'; import { ArithmeticsGeneratedModule, ArithmeticsGeneratedSharedModule } from './generated/module.js'; @@ -39,7 +40,8 @@ export const ArithmeticsModule: Module new ArithmeticsValidator() }, lsp: { - CodeActionProvider: () => new ArithmeticsCodeActionProvider() + CodeActionProvider: () => new ArithmeticsCodeActionProvider(), + Formatter: () => new ArithmeticsFormatter() } }; diff --git a/examples/arithmetics/src/language-server/arithmetics.langium b/examples/arithmetics/src/language-server/arithmetics.langium index 9c7e40c81..0815913ef 100644 --- a/examples/arithmetics/src/language-server/arithmetics.langium +++ b/examples/arithmetics/src/language-server/arithmetics.langium @@ -29,7 +29,7 @@ infix BinaryExpression on PrimaryExpression: > '+' | '-'; PrimaryExpression infers Expression: - '(' Expression ')' | + {infer NestedExpression} '(' value=Expression ')' | {infer NumberLiteral} value=NUMBER | {infer FunctionCall} func=[AbstractDefinition] ('(' args+=Expression (',' args+=Expression)* ')')?; diff --git a/examples/arithmetics/src/language-server/generated/ast.ts b/examples/arithmetics/src/language-server/generated/ast.ts index bc80c5cd1..1aeacecce 100644 --- a/examples/arithmetics/src/language-server/generated/ast.ts +++ b/examples/arithmetics/src/language-server/generated/ast.ts @@ -44,7 +44,7 @@ export function isAbstractDefinition(item: unknown): item is AbstractDefinition } export interface BinaryExpression extends langium.AstNode { - readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall; + readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | NestedExpression; readonly $type: 'BinaryExpression'; left: Expression; operator: '%' | '*' | '+' | '-' | '/' | '^'; @@ -111,7 +111,7 @@ export function isEvaluation(item: unknown): item is Evaluation { return reflection.isInstance(item, Evaluation.$type); } -export type Expression = BinaryExpression | FunctionCall | NumberLiteral; +export type Expression = BinaryExpression | FunctionCall | NestedExpression | NumberLiteral; export const Expression = { $type: 'Expression' @@ -122,7 +122,7 @@ export function isExpression(item: unknown): item is Expression { } export interface FunctionCall extends langium.AstNode { - readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall; + readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | NestedExpression; readonly $type: 'FunctionCall'; args: Array; func: langium.Reference; @@ -154,8 +154,23 @@ export function isModule(item: unknown): item is Module { return reflection.isInstance(item, Module.$type); } +export interface NestedExpression extends langium.AstNode { + readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | NestedExpression; + readonly $type: 'NestedExpression'; + value: Expression; +} + +export const NestedExpression = { + $type: 'NestedExpression', + value: 'value' +} as const; + +export function isNestedExpression(item: unknown): item is NestedExpression { + return reflection.isInstance(item, NestedExpression.$type); +} + export interface NumberLiteral extends langium.AstNode { - readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall; + readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | NestedExpression; readonly $type: 'NumberLiteral'; value: number; } @@ -188,6 +203,7 @@ export type ArithmeticsAstType = { Expression: Expression FunctionCall: FunctionCall Module: Module + NestedExpression: NestedExpression NumberLiteral: NumberLiteral Statement: Statement } @@ -282,6 +298,15 @@ export class ArithmeticsAstReflection extends langium.AbstractAstReflection { }, superTypes: [] }, + NestedExpression: { + name: NestedExpression.$type, + properties: { + value: { + name: NestedExpression.value + } + }, + superTypes: [Expression.$type] + }, NumberLiteral: { name: NumberLiteral.$type, properties: { diff --git a/examples/arithmetics/src/language-server/generated/grammar.ts b/examples/arithmetics/src/language-server/generated/grammar.ts index a1c33308c..dbf320955 100644 --- a/examples/arithmetics/src/language-server/generated/grammar.ts +++ b/examples/arithmetics/src/language-server/generated/grammar.ts @@ -310,16 +310,28 @@ export const ArithmeticsGrammar = (): Grammar => loadedArithmeticsGrammar ?? (lo { "$type": "Group", "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "NestedExpression" + } + }, { "$type": "Keyword", "value": "(" }, { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [] + } }, { "$type": "Keyword", diff --git a/examples/arithmetics/test/arithmetics-formatting.test.ts b/examples/arithmetics/test/arithmetics-formatting.test.ts new file mode 100644 index 000000000..a2e4badd3 --- /dev/null +++ b/examples/arithmetics/test/arithmetics-formatting.test.ts @@ -0,0 +1,127 @@ +/****************************************************************************** + * Copyright 2025 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import { describe, test } from 'vitest'; +import { EmptyFileSystem } from 'langium'; +import { expectFormatting } from 'langium/test'; +import { createArithmeticsServices } from '../src/language-server/arithmetics-module.js'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const services = createArithmeticsServices({ ...EmptyFileSystem }).arithmetics; +const formatting = expectFormatting(services); + +describe('Arithmetics formatting', () => { + + test('Should preserve well-formatted example.calc content', async () => { + const examplePath = resolve(__dirname, '../example/example.calc'); + const exampleContent = readFileSync(examplePath, 'utf-8'); + + await formatting({ + before: exampleContent, + after: exampleContent + }); + }); + + test('Should format module declaration correctly', async () => { + await formatting({ + before: 'Module basicMath def a:5;', + after: `Module basicMath +def a: 5;` + }); + }); + + test('Should keep each definition on a separate line', async () => { + await formatting({ + before: 'Module test\ndef a:5;\ndef b : 3;', + after: `Module test +def a: 5; +def b: 3;` + }); + }); + + test('Should format parentheses of nested expressions', async () => { + await formatting({ + before: `Module test +def result: ( a + ( b ) ) * c^( 1/a); +def root(x, y):x^( 1/y); +def reuse(a,b):( root ( a,b ) ) ; + ( result + root ( result ,reuse ( 2 , 3 ) ) )/2;`, + after: `Module test +def result: (a + (b)) * c^(1/a); +def root(x, y): + x^(1/y); +def reuse(a, b): + (root(a, b)); +(result + root(result, reuse(2, 3)))/2;` + }); + }); + + test('Should have space after colon for expression definitions', async () => { + await formatting({ + before: `Module test +def d:(x*y);`, + after: `Module test +def d: (x*y);` + }); + }); + + test('Should handle function calls with no spaces around parentheses and a single space after comma', async () => { + await formatting({ + before: `Module test +def a: 5; +root ( a , 2 ); +sqrt( x );`, + after: `Module test +def a: 5; +root(a, 2); +sqrt(x);` + }); + }); + + test('Should format function definitions with no spaces around parentheses and a single space after comma', async () => { + await formatting({ + before: `Module test +def root( x ,y, z ): + x^y;`, + after: `Module test +def root(x, y, z): + x^y;` + }); + }); + + test('Should format nested expressions with proper parentheses spacing (preserving operator spacing)', async () => { + await formatting({ + before: `Module test +def complex: ( ( a + b ) * ( c - d ) ) + ( e / f );`, + after: `Module test +def complex: ((a + b) * (c - d)) + (e / f);` + }); + }); + + test('Should format statements with proper semicolon spacing', async () => { + await formatting({ + before: `Module test +def a: 5; +def root(x, y, z): + x^y ; +b % 2 ;`, + after: `Module test +def a: 5; +def root(x, y, z): + x^y; +b % 2;` + }); + }); + + test('Should preserve extra empty lines and comments', async () => { + const multilineArithmetics = 'Module test\n\ndef a: 5;// this is a comment\n// Another comment\n\ndef root(x, y):\n x^(1/y);\n\n2*a;'; + await formatting({ + before: multilineArithmetics, + after: multilineArithmetics + }); + }); +}); diff --git a/examples/arithmetics/test/arithmetics-parsing.test.ts b/examples/arithmetics/test/arithmetics-parsing.test.ts index 21317e8ba..c027ceccb 100644 --- a/examples/arithmetics/test/arithmetics-parsing.test.ts +++ b/examples/arithmetics/test/arithmetics-parsing.test.ts @@ -4,11 +4,11 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { EmptyFileSystem } from 'langium'; import { describe, expect, test } from 'vitest'; import { createArithmeticsServices } from '../src/language-server/arithmetics-module.js'; -import { EmptyFileSystem } from 'langium'; import type { Evaluation, Module } from '../src/language-server/generated/ast.js'; -import { isBinaryExpression, isFunctionCall, isNumberLiteral, type Expression } from '../src/language-server/generated/ast.js'; +import { isBinaryExpression, isFunctionCall, isNestedExpression, isNumberLiteral, type Expression } from '../src/language-server/generated/ast.js'; describe('Test the arithmetics parsing', () => { @@ -22,6 +22,8 @@ describe('Test the arithmetics parsing', () => { return expr.value.toString(); } else if (isFunctionCall(expr)) { return expr.func.$refText; + } else if (isNestedExpression(expr)) { + return '(' + printExpression(expr.value) + ')'; } return ''; } @@ -45,6 +47,6 @@ describe('Test the arithmetics parsing', () => { const expr = parseExpression('(1 + 2) ^ 3'); // Assert that the nested expression is correctly represented in the AST // If the expression parsing would be too eager, the result would be (1 + (2 ^ 3)) - expect(printExpression(expr)).toBe('((1 + 2) ^ 3)'); + expect(printExpression(expr)).toBe('(((1 + 2)) ^ 3)'); }); });