From dd610ddf46ad7dacc125db8f5bb81130efac4aeb Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Fri, 19 Sep 2025 14:36:32 +0100 Subject: [PATCH 01/10] Generated formatter for Arithmetics --- .../language-server/arithmetics-formatter.ts | 59 +++++++++++++++++++ .../src/language-server/arithmetics-module.ts | 4 +- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 examples/arithmetics/src/language-server/arithmetics-formatter.ts 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..1a4677742 --- /dev/null +++ b/examples/arithmetics/src/language-server/arithmetics-formatter.ts @@ -0,0 +1,59 @@ +/****************************************************************************** + * 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 } 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()); + // 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); + // Format definition: "def" keyword followed by space + formatter.keyword('def').append(Formatting.oneSpace()); + // Space before colon and after colon + formatter.keyword(':').surround(Formatting.oneSpace()); + // No space before semicolon + formatter.keyword(';').prepend(Formatting.noSpace()); + // Format parentheses around parameters (if any) + if (node.args.length > 0) { + formatter.keyword('(').append(Formatting.noSpace()); + formatter.keyword(')').prepend(Formatting.noSpace()); + // Space after commas in parameter lists + formatter.keywords(',').append(Formatting.oneSpace()); + } + } else if (ast.isEvaluation(node)) { + const formatter = this.getNodeFormatter(node); + // No space before semicolon + formatter.keyword(';').prepend(Formatting.noSpace()); + } else if (ast.isBinaryExpression(node)) { + const formatter = this.getNodeFormatter(node); + // Spaces around all binary operators (+, -, *, /, %, ^) + formatter.keywords('+', '-', '*', '/', '%', '^').surround(Formatting.oneSpace()); + } else if (ast.isFunctionCall(node)) { + const formatter = this.getNodeFormatter(node); + // Format parentheses and arguments (if any) + if (node.args.length > 0) { + formatter.keyword('(').append(Formatting.noSpace()); + formatter.keyword(')').prepend(Formatting.noSpace()); + // Space after commas in argument lists + formatter.keywords(',').append(Formatting.oneSpace()); + } + } + + // Handle parentheses in expressions - no space inside parentheses + const formatter = this.getNodeFormatter(node); + formatter.keyword('(').append(Formatting.noSpace()); + formatter.keyword(')').prepend(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() } }; From c76d53894f06941f5f97e63f3903c3d9e45b666c Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Fri, 19 Sep 2025 18:59:19 +0100 Subject: [PATCH 02/10] Elaborate formatting within statements Make function definitions two lines, while parameter definitions one line --- .../language-server/arithmetics-formatter.ts | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/examples/arithmetics/src/language-server/arithmetics-formatter.ts b/examples/arithmetics/src/language-server/arithmetics-formatter.ts index 1a4677742..a1d55e655 100644 --- a/examples/arithmetics/src/language-server/arithmetics-formatter.ts +++ b/examples/arithmetics/src/language-server/arithmetics-formatter.ts @@ -14,46 +14,52 @@ export class ArithmeticsFormatter extends AbstractFormatter { 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); - // Format definition: "def" keyword followed by space formatter.keyword('def').append(Formatting.oneSpace()); - // Space before colon and after colon - formatter.keyword(':').surround(Formatting.oneSpace()); - // No space before semicolon - formatter.keyword(';').prepend(Formatting.noSpace()); - // Format parentheses around parameters (if any) + formatter.keyword(':').prepend(Formatting.noSpace()); + + // Format Definition of a function if (node.args.length > 0) { - formatter.keyword('(').append(Formatting.noSpace()); - formatter.keyword(')').prepend(Formatting.noSpace()); + formatter.keywords('(', ')').surround(Formatting.noSpace()); // Space after commas in parameter lists formatter.keywords(',').append(Formatting.oneSpace()); + formatter.property('expr').prepend(Formatting.indent()); + // Format Definition of a constant + } else { + formatter.property('expr').prepend(Formatting.oneSpace()); } } else if (ast.isEvaluation(node)) { const formatter = this.getNodeFormatter(node); // No space before semicolon formatter.keyword(';').prepend(Formatting.noSpace()); - } else if (ast.isBinaryExpression(node)) { + } else if (ast.isExpression(node)) { const formatter = this.getNodeFormatter(node); - // Spaces around all binary operators (+, -, *, /, %, ^) - formatter.keywords('+', '-', '*', '/', '%', '^').surround(Formatting.oneSpace()); + formatter.keyword('(').append(Formatting.noSpace()); + formatter.keyword(')').prepend(Formatting.noSpace()); + if (ast.isBinaryExpression(node)) { + // const formatter = this.getNodeFormatter(node); + // Spaces around all binary operators + // TODO: Infix rules cannot be formatted + // operators cannot be formatted neither as keywords nor as properties + // left/right property cannot be formatted either + } } else if (ast.isFunctionCall(node)) { const formatter = this.getNodeFormatter(node); // Format parentheses and arguments (if any) if (node.args.length > 0) { - formatter.keyword('(').append(Formatting.noSpace()); - formatter.keyword(')').prepend(Formatting.noSpace()); + formatter.keywords('(', ')').surround(Formatting.noSpace()); // Space after commas in argument lists formatter.keywords(',').append(Formatting.oneSpace()); } } - // Handle parentheses in expressions - no space inside parentheses + // No space around semicolons in all cases const formatter = this.getNodeFormatter(node); - formatter.keyword('(').append(Formatting.noSpace()); - formatter.keyword(')').prepend(Formatting.noSpace()); + formatter.keyword(';').surround(Formatting.noSpace()); } } From 1b9430a642a9f751324cbc787c9945be2faba12e Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Thu, 25 Sep 2025 18:32:57 +0100 Subject: [PATCH 03/10] Attempted fixing binary expression formatting issue --- .../src/language-server/arithmetics-formatter.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/arithmetics/src/language-server/arithmetics-formatter.ts b/examples/arithmetics/src/language-server/arithmetics-formatter.ts index a1d55e655..0720fd1e0 100644 --- a/examples/arithmetics/src/language-server/arithmetics-formatter.ts +++ b/examples/arithmetics/src/language-server/arithmetics-formatter.ts @@ -23,30 +23,34 @@ export class ArithmeticsFormatter extends AbstractFormatter { formatter.keyword('def').append(Formatting.oneSpace()); formatter.keyword(':').prepend(Formatting.noSpace()); - // Format Definition of a function + // TODO: Incorrectly handles function definitions with single _ as a parameter if (node.args.length > 0) { + // Format Definition of a function formatter.keywords('(', ')').surround(Formatting.noSpace()); - // Space after commas in parameter lists formatter.keywords(',').append(Formatting.oneSpace()); formatter.property('expr').prepend(Formatting.indent()); - // Format Definition of a constant } else { + // Format Definition of a constant formatter.property('expr').prepend(Formatting.oneSpace()); } + } else if (ast.isEvaluation(node)) { const formatter = this.getNodeFormatter(node); // No space before semicolon formatter.keyword(';').prepend(Formatting.noSpace()); } else if (ast.isExpression(node)) { const formatter = this.getNodeFormatter(node); + // Keep parentheses tight with no spaces inside formatter.keyword('(').append(Formatting.noSpace()); formatter.keyword(')').prepend(Formatting.noSpace()); + + // For binary expressions, try to keep everything on one line if (ast.isBinaryExpression(node)) { // const formatter = this.getNodeFormatter(node); - // Spaces around all binary operators // TODO: Infix rules cannot be formatted // operators cannot be formatted neither as keywords nor as properties // left/right property cannot be formatted either + // formatter.node(node.operator).surround(getOperatorSpacing(node.operator)); } } else if (ast.isFunctionCall(node)) { const formatter = this.getNodeFormatter(node); From 6b88f6694432aeedff831f49b971d77cb8344893 Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Thu, 25 Sep 2025 19:51:56 +0100 Subject: [PATCH 04/10] Provide Langium copyright snippet and peacock workspace settings --- .vscode/global.code-snippets | 15 +++++++++++++++ .vscode/settings.json | 23 ++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .vscode/global.code-snippets 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", } From 58a68dba264e395c2b1a2a544ab11ab71d4a2da0 Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Thu, 25 Sep 2025 20:09:14 +0100 Subject: [PATCH 05/10] Cover statement formatting with tests --- examples/arithmetics/example/example.calc | 2 +- .../test/arithmetics-formatting.test.ts | 116 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 examples/arithmetics/test/arithmetics-formatting.test.ts diff --git a/examples/arithmetics/example/example.calc b/examples/arithmetics/example/example.calc index e2af2f09f..74df8526d 100644 --- a/examples/arithmetics/example/example.calc +++ b/examples/arithmetics/example/example.calc @@ -3,7 +3,7 @@ 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); diff --git a/examples/arithmetics/test/arithmetics-formatting.test.ts b/examples/arithmetics/test/arithmetics-formatting.test.ts new file mode 100644 index 000000000..241768c5b --- /dev/null +++ b/examples/arithmetics/test/arithmetics-formatting.test.ts @@ -0,0 +1,116 @@ +/****************************************************************************** + * 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', () => { + + // Note: The current formatter preserves existing spacing around commas in parameter lists + // due to a known limitation (see TODO comment in ArithmeticsFormatter) + + 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 format constant definitions with proper spacing', async () => { + await formatting({ + before: 'Module test\ndef a:5;\ndef b : 3;', + after: `Module test +def a: 5; +def b: 3;` + }); + }); + + test('Should format function definitions with parameters (preserving existing comma spacing)', async () => { + await formatting({ + before: 'Module test\ndef root( x, y ):\n x^(1/y);', + after: `Module test +def root(x, y): + x^(1/y);` + }); + }); + + test('Should format function calls with proper spacing (preserving existing comma spacing)', async () => { + await formatting({ + before: 'Module test\ndef a: 5;\nroot( a, 2 );', + after: `Module test +def a: 5; +root(a, 2);` + }); + }); + + test('Should format binary expressions with parentheses correctly', async () => { + await formatting({ + before: 'Module test\ndef result: ( a + b ) * c;', + after: `Module test +def result: (a + b) * c;` + }); + }); + + test('Should format evaluation statements correctly', async () => { + await formatting({ + before: 'Module test\n def a\n:\n 5 \n; \n2 * a ;', + after: `Module test +def a: 5; +2 * a;` + }); + }); + + test('Should handle mixed formatting issues (preserving empty lines between statements and comma spacing)', async () => { + await formatting({ + before: `Module test + +def a : 5 ; +def root( x, y ) : + x^(1/y) ; + +2*a ;`, + after: `Module test + +def a: 5; +def root(x, y): + x^(1/y); + +2*a;` + }); + }); + + test('Should preserve comments', async () => { + await formatting({ + before: `Module test +def a: 5; // this is a comment +// Another comment +def b: 3;`, + after: `Module test +def a: 5; // this is a comment +// Another comment +def b: 3;` + }); + }); + +}); From 719dd090f5d66d7841c7a43719ac0bd96640e4c7 Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Thu, 25 Sep 2025 20:56:56 +0100 Subject: [PATCH 06/10] Fix parameters formatting --- examples/arithmetics/example/example.calc | 6 +- .../language-server/arithmetics-formatter.ts | 27 +++---- .../test/arithmetics-formatting.test.ts | 81 +++++++++++++------ 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/examples/arithmetics/example/example.calc b/examples/arithmetics/example/example.calc index 74df8526d..6ae161c14 100644 --- a/examples/arithmetics/example/example.calc +++ b/examples/arithmetics/example/example.calc @@ -8,6 +8,9 @@ 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/src/language-server/arithmetics-formatter.ts b/examples/arithmetics/src/language-server/arithmetics-formatter.ts index 0720fd1e0..6586a992b 100644 --- a/examples/arithmetics/src/language-server/arithmetics-formatter.ts +++ b/examples/arithmetics/src/language-server/arithmetics-formatter.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import type { AstNode } from 'langium'; -import { AbstractFormatter, Formatting } from 'langium/lsp'; +import { AbstractFormatter, Formatting, type NodeFormatter } from 'langium/lsp'; import * as ast from './generated/ast.js'; export class ArithmeticsFormatter extends AbstractFormatter { @@ -23,24 +23,21 @@ export class ArithmeticsFormatter extends AbstractFormatter { formatter.keyword('def').append(Formatting.oneSpace()); formatter.keyword(':').prepend(Formatting.noSpace()); - // TODO: Incorrectly handles function definitions with single _ as a parameter if (node.args.length > 0) { // Format Definition of a function - formatter.keywords('(', ')').surround(Formatting.noSpace()); - formatter.keywords(',').append(Formatting.oneSpace()); + formatParameters(formatter); formatter.property('expr').prepend(Formatting.indent()); } else { // Format Definition of a constant formatter.property('expr').prepend(Formatting.oneSpace()); } - } else if (ast.isEvaluation(node)) { + } else if (ast.isFunctionCall(node)) { const formatter = this.getNodeFormatter(node); - // No space before semicolon - formatter.keyword(';').prepend(Formatting.noSpace()); + formatParameters(formatter); } else if (ast.isExpression(node)) { const formatter = this.getNodeFormatter(node); - // Keep parentheses tight with no spaces inside + // Keep parentheses tight with no spaces inside (but don't restrict spaces outside) formatter.keyword('(').append(Formatting.noSpace()); formatter.keyword(')').prepend(Formatting.noSpace()); @@ -52,14 +49,6 @@ export class ArithmeticsFormatter extends AbstractFormatter { // left/right property cannot be formatted either // formatter.node(node.operator).surround(getOperatorSpacing(node.operator)); } - } else if (ast.isFunctionCall(node)) { - const formatter = this.getNodeFormatter(node); - // Format parentheses and arguments (if any) - if (node.args.length > 0) { - formatter.keywords('(', ')').surround(Formatting.noSpace()); - // Space after commas in argument lists - formatter.keywords(',').append(Formatting.oneSpace()); - } } // No space around semicolons in all cases @@ -67,3 +56,9 @@ export class ArithmeticsFormatter extends AbstractFormatter { 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 })); +} diff --git a/examples/arithmetics/test/arithmetics-formatting.test.ts b/examples/arithmetics/test/arithmetics-formatting.test.ts index 241768c5b..6c5fb50a1 100644 --- a/examples/arithmetics/test/arithmetics-formatting.test.ts +++ b/examples/arithmetics/test/arithmetics-formatting.test.ts @@ -16,9 +16,6 @@ const formatting = expectFormatting(services); describe('Arithmetics formatting', () => { - // Note: The current formatter preserves existing spacing around commas in parameter lists - // due to a known limitation (see TODO comment in ArithmeticsFormatter) - test('Should preserve well-formatted example.calc content', async () => { const examplePath = resolve(__dirname, '../example/example.calc'); const exampleContent = readFileSync(examplePath, 'utf-8'); @@ -37,7 +34,7 @@ def a: 5;` }); }); - test('Should format constant definitions with proper spacing', async () => { + test('Should keep each definition on a separate line', async () => { await formatting({ before: 'Module test\ndef a:5;\ndef b : 3;', after: `Module test @@ -46,53 +43,90 @@ def b: 3;` }); }); - test('Should format function definitions with parameters (preserving existing comma spacing)', async () => { + test('Should format parentheses with no surrounding spaces', async () => { await formatting({ - before: 'Module test\ndef root( x, y ):\n x^(1/y);', + before: `Module test +def result: ( a + b ) * c; +def nested: ( ( a + b ) * ( c - d ) );`, after: `Module test -def root(x, y): - x^(1/y);` +def result: (a + b) * c; +def nested: ((a + b) * (c - d));` }); }); - test('Should format function calls with proper spacing (preserving existing comma spacing)', async () => { + test('Should have space after colon for expression definitions', async () => { await formatting({ - before: 'Module test\ndef a: 5;\nroot( a, 2 );', + 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);` +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 binary expressions with parentheses correctly', async () => { + test('Should format nested expressions with proper parentheses spacing (preserving operator spacing)', async () => { await formatting({ - before: 'Module test\ndef result: ( a + b ) * c;', + before: `Module test +def complex: ( ( a + b ) * ( c - d ) ) + ( e / f );`, after: `Module test -def result: (a + b) * c;` +def complex: ((a + b) * (c - d)) + (e / f);` }); }); - test('Should format evaluation statements correctly', async () => { + test('Should format statements with proper semicolon spacing', async () => { await formatting({ - before: 'Module test\n def a\n:\n 5 \n; \n2 * a ;', + before: `Module test +def a: 5; +def root(x, y, z): + x^y ; +b % 2 ;`, after: `Module test def a: 5; -2 * a;` +def root(x, y, z): + x^y; +b % 2;` }); }); - test('Should handle mixed formatting issues (preserving empty lines between statements and comma spacing)', async () => { + test('Should preserve extra empty lines, but not extra spaces ', async () => { await formatting({ before: `Module test def a : 5 ; -def root( x, y ) : + + +def root( x, y ) : x^(1/y) ; -2*a ;`, +2*a ; `, after: `Module test def a: 5; + + def root(x, y): x^(1/y); @@ -103,14 +137,13 @@ def root(x, y): test('Should preserve comments', async () => { await formatting({ before: `Module test -def a: 5; // this is a comment -// Another comment +def a: 5; // this is a comment +// Another comment def b: 3;`, after: `Module test def a: 5; // this is a comment -// Another comment +// Another comment def b: 3;` }); }); - }); From ec89f2c220bebd147f60ce8a3d80ab52159857b5 Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Thu, 25 Sep 2025 23:40:41 +0100 Subject: [PATCH 07/10] Format grouping parens differently than the function parens --- .../language-server/arithmetics-formatter.ts | 35 +++++++++++++------ .../src/language-server/arithmetics.langium | 2 +- .../src/language-server/generated/ast.ts | 33 ++++++++++++++--- .../src/language-server/generated/grammar.ts | 22 +++++++++--- .../test/arithmetics-formatting.test.ts | 14 +++++--- 5 files changed, 81 insertions(+), 25 deletions(-) diff --git a/examples/arithmetics/src/language-server/arithmetics-formatter.ts b/examples/arithmetics/src/language-server/arithmetics-formatter.ts index 6586a992b..5eccb2dcd 100644 --- a/examples/arithmetics/src/language-server/arithmetics-formatter.ts +++ b/examples/arithmetics/src/language-server/arithmetics-formatter.ts @@ -35,20 +35,19 @@ export class ArithmeticsFormatter extends AbstractFormatter { } else if (ast.isFunctionCall(node)) { const formatter = this.getNodeFormatter(node); formatParameters(formatter); - } else if (ast.isExpression(node)) { + } else if (ast.isGroupedExpression(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()); - - // For binary expressions, try to keep everything on one line - if (ast.isBinaryExpression(node)) { - // const formatter = this.getNodeFormatter(node); - // TODO: Infix rules cannot be formatted - // operators cannot be formatted neither as keywords nor as properties - // left/right property cannot be formatted either - // formatter.node(node.operator).surround(getOperatorSpacing(node.operator)); - } + } else if (ast.isBinaryExpression(node)) { + // const formatter = this.getNodeFormatter(node); + // TODO: Infix rules cannot be formatted + // operators cannot be formatted neither as keywords nor as properties + // left/right property cannot be formatted either + // const leftNode = formatter.node(node.left); + // leftNode.append(getOperatorSpacing(node.operator)); + // formatter.node(node.right).prepend(getOperatorSpacing(node.operator)); } // No space around semicolons in all cases @@ -62,3 +61,19 @@ function formatParameters(formatter: NodeFormatter '+' | '-'; PrimaryExpression infers Expression: - '(' Expression ')' | + {infer GroupedExpression} '(' 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..532d45cc9 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 | GroupedExpression; 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 | GroupedExpression | 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 | GroupedExpression; readonly $type: 'FunctionCall'; args: Array; func: langium.Reference; @@ -138,6 +138,21 @@ export function isFunctionCall(item: unknown): item is FunctionCall { return reflection.isInstance(item, FunctionCall.$type); } +export interface GroupedExpression extends langium.AstNode { + readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | GroupedExpression; + readonly $type: 'GroupedExpression'; + value: Expression; +} + +export const GroupedExpression = { + $type: 'GroupedExpression', + value: 'value' +} as const; + +export function isGroupedExpression(item: unknown): item is GroupedExpression { + return reflection.isInstance(item, GroupedExpression.$type); +} + export interface Module extends langium.AstNode { readonly $type: 'Module'; name: string; @@ -155,7 +170,7 @@ export function isModule(item: unknown): item is Module { } export interface NumberLiteral extends langium.AstNode { - readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall; + readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | GroupedExpression; readonly $type: 'NumberLiteral'; value: number; } @@ -187,6 +202,7 @@ export type ArithmeticsAstType = { Evaluation: Evaluation Expression: Expression FunctionCall: FunctionCall + GroupedExpression: GroupedExpression Module: Module NumberLiteral: NumberLiteral Statement: Statement @@ -269,6 +285,15 @@ export class ArithmeticsAstReflection extends langium.AbstractAstReflection { }, superTypes: [Expression.$type] }, + GroupedExpression: { + name: GroupedExpression.$type, + properties: { + value: { + name: GroupedExpression.value + } + }, + superTypes: [Expression.$type] + }, Module: { name: Module.$type, properties: { diff --git a/examples/arithmetics/src/language-server/generated/grammar.ts b/examples/arithmetics/src/language-server/generated/grammar.ts index a1c33308c..c700ba3d6 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": "GroupedExpression" + } + }, { "$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 index 6c5fb50a1..721f739dc 100644 --- a/examples/arithmetics/test/arithmetics-formatting.test.ts +++ b/examples/arithmetics/test/arithmetics-formatting.test.ts @@ -43,14 +43,18 @@ def b: 3;` }); }); - test('Should format parentheses with no surrounding spaces', async () => { + test('Should format parentheses of grouped expressions', async () => { await formatting({ before: `Module test -def result: ( a + b ) * c; -def nested: ( ( a + b ) * ( c - d ) );`, +def result: ( a + b ) * c^( 1/a); +def root(x, y):x^( 1/y); +def reuse(a,b):( root ( a,b ) ) ;`, after: `Module test -def result: (a + b) * c; -def nested: ((a + b) * (c - d));` +def result: (a + b) * c^(1/a); +def root(x, y): + x^(1/y); +def reuse(a, b): + (root(a, b));` }); }); From 74228eb48d75b5f709a0d8fdca863f75937c316a Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Thu, 25 Sep 2025 23:48:51 +0100 Subject: [PATCH 08/10] Fix failing tests due to Langium not formatting comments --- .../test/arithmetics-formatting.test.ts | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/examples/arithmetics/test/arithmetics-formatting.test.ts b/examples/arithmetics/test/arithmetics-formatting.test.ts index 721f739dc..5f8d91c33 100644 --- a/examples/arithmetics/test/arithmetics-formatting.test.ts +++ b/examples/arithmetics/test/arithmetics-formatting.test.ts @@ -115,39 +115,11 @@ b % 2;` }); }); - test('Should preserve extra empty lines, but not extra spaces ', async () => { + 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: `Module test - -def a : 5 ; - - -def root( x, y ) : - x^(1/y) ; - -2*a ; `, - after: `Module test - -def a: 5; - - -def root(x, y): - x^(1/y); - -2*a;` - }); - }); - - test('Should preserve comments', async () => { - await formatting({ - before: `Module test -def a: 5; // this is a comment -// Another comment -def b: 3;`, - after: `Module test -def a: 5; // this is a comment -// Another comment -def b: 3;` + before: multilineArithmetics, + after: multilineArithmetics }); }); }); From 879cf52c8afff22a17ef7f505a52fd5e6380cb43 Mon Sep 17 00:00:00 2001 From: Gregory Popov Date: Fri, 26 Sep 2025 00:55:58 +0100 Subject: [PATCH 09/10] Discover formatting binary expression issue root cause --- examples/arithmetics/example/example.calc | 2 +- .../arithmetics/example/infix-rule-bug.calc | 7 ++++++ .../arithmetics/example/poor example.calc | 21 ++++++++++++++++ .../language-server/arithmetics-formatter.ts | 24 ++++++++++++------- 4 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 examples/arithmetics/example/infix-rule-bug.calc create mode 100644 examples/arithmetics/example/poor example.calc diff --git a/examples/arithmetics/example/example.calc b/examples/arithmetics/example/example.calc index 6ae161c14..40e735d8d 100644 --- a/examples/arithmetics/example/example.calc +++ b/examples/arithmetics/example/example.calc @@ -21,4 +21,4 @@ b % 2; // 1 Root(D, 3); // 32 Root(64, 3); // 4 Sqrt(81); // 9 -empty(a+b); // 0 \ No newline at end of file +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-formatter.ts b/examples/arithmetics/src/language-server/arithmetics-formatter.ts index 5eccb2dcd..1bd308d88 100644 --- a/examples/arithmetics/src/language-server/arithmetics-formatter.ts +++ b/examples/arithmetics/src/language-server/arithmetics-formatter.ts @@ -22,7 +22,6 @@ export class ArithmeticsFormatter extends AbstractFormatter { 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); @@ -35,18 +34,30 @@ export class ArithmeticsFormatter extends AbstractFormatter { } else if (ast.isFunctionCall(node)) { const formatter = this.getNodeFormatter(node); formatParameters(formatter); - } else if (ast.isGroupedExpression(node)) { + } else if (ast.isGroupedExpression(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); - // TODO: Infix rules cannot be formatted + // 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 - // const leftNode = formatter.node(node.left); - // leftNode.append(getOperatorSpacing(node.operator)); + // formatter.node(node.left).append(getOperatorSpacing(node.operator)); // formatter.node(node.right).prepend(getOperatorSpacing(node.operator)); } @@ -63,8 +74,6 @@ function formatParameters(formatter: NodeFormatter Date: Fri, 26 Sep 2025 02:28:29 +0100 Subject: [PATCH 10/10] Fix tests and use existing NestedExpression term --- .../language-server/arithmetics-evaluator.ts | 5 +- .../language-server/arithmetics-formatter.ts | 2 +- .../src/language-server/arithmetics.langium | 2 +- .../src/language-server/generated/ast.ts | 58 +++++++++---------- .../src/language-server/generated/grammar.ts | 2 +- .../test/arithmetics-formatting.test.ts | 12 ++-- .../test/arithmetics-parsing.test.ts | 8 ++- 7 files changed, 48 insertions(+), 41 deletions(-) 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 index 1bd308d88..ba286043b 100644 --- a/examples/arithmetics/src/language-server/arithmetics-formatter.ts +++ b/examples/arithmetics/src/language-server/arithmetics-formatter.ts @@ -34,7 +34,7 @@ export class ArithmeticsFormatter extends AbstractFormatter { } else if (ast.isFunctionCall(node)) { const formatter = this.getNodeFormatter(node); formatParameters(formatter); - } else if (ast.isGroupedExpression(node)) { + } 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()); diff --git a/examples/arithmetics/src/language-server/arithmetics.langium b/examples/arithmetics/src/language-server/arithmetics.langium index c54df04f8..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: - {infer GroupedExpression} '(' value=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 532d45cc9..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 | GroupedExpression; + 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 | GroupedExpression | 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 | GroupedExpression; + readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | NestedExpression; readonly $type: 'FunctionCall'; args: Array; func: langium.Reference; @@ -138,21 +138,6 @@ export function isFunctionCall(item: unknown): item is FunctionCall { return reflection.isInstance(item, FunctionCall.$type); } -export interface GroupedExpression extends langium.AstNode { - readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | GroupedExpression; - readonly $type: 'GroupedExpression'; - value: Expression; -} - -export const GroupedExpression = { - $type: 'GroupedExpression', - value: 'value' -} as const; - -export function isGroupedExpression(item: unknown): item is GroupedExpression { - return reflection.isInstance(item, GroupedExpression.$type); -} - export interface Module extends langium.AstNode { readonly $type: 'Module'; name: string; @@ -169,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 | GroupedExpression; + readonly $container: BinaryExpression | Definition | Evaluation | FunctionCall | NestedExpression; readonly $type: 'NumberLiteral'; value: number; } @@ -202,8 +202,8 @@ export type ArithmeticsAstType = { Evaluation: Evaluation Expression: Expression FunctionCall: FunctionCall - GroupedExpression: GroupedExpression Module: Module + NestedExpression: NestedExpression NumberLiteral: NumberLiteral Statement: Statement } @@ -285,15 +285,6 @@ export class ArithmeticsAstReflection extends langium.AbstractAstReflection { }, superTypes: [Expression.$type] }, - GroupedExpression: { - name: GroupedExpression.$type, - properties: { - value: { - name: GroupedExpression.value - } - }, - superTypes: [Expression.$type] - }, Module: { name: Module.$type, properties: { @@ -307,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 c700ba3d6..dbf320955 100644 --- a/examples/arithmetics/src/language-server/generated/grammar.ts +++ b/examples/arithmetics/src/language-server/generated/grammar.ts @@ -314,7 +314,7 @@ export const ArithmeticsGrammar = (): Grammar => loadedArithmeticsGrammar ?? (lo "$type": "Action", "inferredType": { "$type": "InferredType", - "name": "GroupedExpression" + "name": "NestedExpression" } }, { diff --git a/examples/arithmetics/test/arithmetics-formatting.test.ts b/examples/arithmetics/test/arithmetics-formatting.test.ts index 5f8d91c33..a2e4badd3 100644 --- a/examples/arithmetics/test/arithmetics-formatting.test.ts +++ b/examples/arithmetics/test/arithmetics-formatting.test.ts @@ -43,18 +43,20 @@ def b: 3;` }); }); - test('Should format parentheses of grouped expressions', async () => { + test('Should format parentheses of nested expressions', async () => { await formatting({ before: `Module test -def result: ( a + b ) * c^( 1/a); +def result: ( a + ( b ) ) * c^( 1/a); def root(x, y):x^( 1/y); -def reuse(a,b):( root ( a,b ) ) ;`, +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 result: (a + (b)) * c^(1/a); def root(x, y): x^(1/y); def reuse(a, b): - (root(a, b));` + (root(a, b)); +(result + root(result, reuse(2, 3)))/2;` }); }); 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)'); }); });