From df68b096b5b622dc7a2bfaa8adb3355b4215c8ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:42:04 +0000 Subject: [PATCH 1/4] Initial plan From f8e9295ca07a1c8e9c82a37a936cd50c12293053 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:53:12 +0000 Subject: [PATCH 2/4] Add LSP document formatting and safe pretty-printer API Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- packages/bridge-parser/src/bridge-printer.ts | 65 ++++++++++++++++--- packages/bridge-parser/src/index.ts | 7 +- packages/bridge-parser/src/parser/index.ts | 1 + packages/bridge-parser/src/parser/parser.ts | 17 +++++ packages/bridge-syntax-highlight/README.md | 2 + packages/bridge-syntax-highlight/package.json | 7 +- .../bridge-syntax-highlight/src/server.ts | 33 +++++++++- packages/bridge/test/bridge-printer.test.ts | 33 +++++++++- 8 files changed, 153 insertions(+), 12 deletions(-) diff --git a/packages/bridge-parser/src/bridge-printer.ts b/packages/bridge-parser/src/bridge-printer.ts index 0fc3d308..6d275f17 100644 --- a/packages/bridge-parser/src/bridge-printer.ts +++ b/packages/bridge-parser/src/bridge-printer.ts @@ -7,8 +7,30 @@ */ import type { IToken } from "chevrotain"; import { BridgeLexer } from "./parser/lexer.ts"; - -const INDENT = " "; +import { parseBridgeCst } from "./parser/parser.ts"; + +const DEFAULT_FORMATTING_OPTIONS = { + tabSize: 2, + insertSpaces: true, +} as const; + +export type BridgeFormattingOptions = { + tabSize: number; + insertSpaces: boolean; +}; + +function resolveFormattingOptions( + options?: Partial, +): BridgeFormattingOptions { + const tabSize = Number.isInteger(options?.tabSize) + ? Math.max(1, options?.tabSize ?? DEFAULT_FORMATTING_OPTIONS.tabSize) + : DEFAULT_FORMATTING_OPTIONS.tabSize; + + return { + tabSize, + insertSpaces: options?.insertSpaces ?? DEFAULT_FORMATTING_OPTIONS.insertSpaces, + }; +} // ── Comment handling ───────────────────────────────────────────────────────── @@ -145,7 +167,22 @@ function isTopLevelBlockStart(group: IToken[]): boolean { * @param source - The Bridge DSL source text to format * @returns Formatted source text, or the original if parsing fails */ -export function formatBridge(source: string): string { +export function prettyPrintToSource( + source: string, + options?: Partial, +): string { + parseBridgeCst(source); + return formatBridgeInternal(source, options); +} + +function formatBridgeInternal( + source: string, + options?: Partial, +): string { + const formatting = resolveFormattingOptions(options); + const indentUnit = formatting.insertSpaces + ? " ".repeat(formatting.tabSize) + : "\t"; const lexResult = BridgeLexer.tokenize(source); if (lexResult.errors.length > 0) { @@ -313,7 +350,7 @@ export function formatBridge(source: string): string { if (lastCommentLine > 0 && commentLine > lastCommentLine + 1) { output.push("\n"); // Preserve blank line between comments } - output.push(INDENT.repeat(lineStartDepth) + comment.image + "\n"); + output.push(indentUnit.repeat(lineStartDepth) + comment.image + "\n"); lastCommentLine = commentLine; } } @@ -371,7 +408,7 @@ export function formatBridge(source: string): string { ); if (hasContentAfter && lineOutput.length > 0) { // Emit the line with the brace, content will continue on next iteration - output.push(INDENT.repeat(currentIndent) + lineOutput + "\n"); + output.push(indentUnit.repeat(currentIndent) + lineOutput + "\n"); lineOutput = ""; lastType = null; currentIndent = depth; // Update indentation for remaining content @@ -390,7 +427,7 @@ export function formatBridge(source: string): string { // Output anything accumulated first if (lineOutput.length > 0) { - output.push(INDENT.repeat(depth) + lineOutput + "\n"); + output.push(indentUnit.repeat(depth) + lineOutput + "\n"); lineOutput = ""; } // Decrement depth, then emit brace at new (outer) depth @@ -405,7 +442,7 @@ export function formatBridge(source: string): string { } // Emit the closing brace immediately - output.push(INDENT.repeat(depth) + braceOutput + "\n"); + output.push(indentUnit.repeat(depth) + braceOutput + "\n"); continue; } @@ -458,7 +495,7 @@ export function formatBridge(source: string): string { // Emit the line if (lineOutput.length > 0) { - output.push(INDENT.repeat(currentIndent) + lineOutput + "\n"); + output.push(indentUnit.repeat(currentIndent) + lineOutput + "\n"); } lastOutputLine = originalLine; @@ -479,3 +516,15 @@ export function formatBridge(source: string): string { return result; } + +/** + * Backward-compatible formatter API. + * + * Returns the original source when syntax is invalid. + */ +export function formatBridge( + source: string, + options?: Partial, +): string { + return formatBridgeInternal(source, options); +} diff --git a/packages/bridge-parser/src/index.ts b/packages/bridge-parser/src/index.ts index d2568e65..df31269f 100644 --- a/packages/bridge-parser/src/index.ts +++ b/packages/bridge-parser/src/index.ts @@ -10,6 +10,7 @@ export { parseBridgeChevrotain as parseBridge, parseBridgeChevrotain, + parseBridgeCst, parseBridgeDiagnostics, PARSER_VERSION, } from "./parser/index.ts"; @@ -25,7 +26,11 @@ export { // ── Formatter ─────────────────────────────────────────────────────────────── -export { formatBridge } from "./bridge-printer.ts"; +export { + formatBridge, + prettyPrintToSource, + type BridgeFormattingOptions, +} from "./bridge-printer.ts"; // ── Language service ──────────────────────────────────────────────────────── diff --git a/packages/bridge-parser/src/parser/index.ts b/packages/bridge-parser/src/parser/index.ts index 6ce27ea6..7ab36947 100644 --- a/packages/bridge-parser/src/parser/index.ts +++ b/packages/bridge-parser/src/parser/index.ts @@ -5,6 +5,7 @@ */ export { parseBridgeChevrotain, + parseBridgeCst, parseBridgeDiagnostics, PARSER_VERSION, } from "./parser.ts"; diff --git a/packages/bridge-parser/src/parser/parser.ts b/packages/bridge-parser/src/parser/parser.ts index 990efbab..8211eb0c 100644 --- a/packages/bridge-parser/src/parser/parser.ts +++ b/packages/bridge-parser/src/parser/parser.ts @@ -1240,6 +1240,23 @@ export function parseBridgeChevrotain(text: string): BridgeDocument { return internalParse(text); } +export function parseBridgeCst(text: string): CstNode { + const lexResult = BridgeLexer.tokenize(text); + if (lexResult.errors.length > 0) { + const e = lexResult.errors[0]; + throw new Error(`Line ${e.line}: Unexpected character "${e.message}"`); + } + + parserInstance.input = lexResult.tokens; + const cst = parserInstance.program(); + if (parserInstance.errors.length > 0) { + const e = parserInstance.errors[0]; + throw new Error(e.message); + } + + return cst; +} + // ── Diagnostic types ────────────────────────────────────────────────────── export type BridgeDiagnostic = { diff --git a/packages/bridge-syntax-highlight/README.md b/packages/bridge-syntax-highlight/README.md index 7d40b369..4664de02 100644 --- a/packages/bridge-syntax-highlight/README.md +++ b/packages/bridge-syntax-highlight/README.md @@ -15,6 +15,8 @@ Full IDE support for [The Bridge](https://github.com/stackables/bridge): a decla - Tool hover: function name, deps, wires - Define hover: subgraph details - Const hover: name and raw value +- **Document formatting** — format `.bridge` files through the standard `textDocument/formatting` LSP request +- Formatter respects your local editor settings (`editor.tabSize`, `editor.insertSpaces`) and works with `editor.formatOnSave` - Error recovery — partial AST is built even on broken files, so diagnostics remain accurate while you're mid-edit ### Syntax Highlighting diff --git a/packages/bridge-syntax-highlight/package.json b/packages/bridge-syntax-highlight/package.json index e84cd5e5..58f6b3b6 100644 --- a/packages/bridge-syntax-highlight/package.json +++ b/packages/bridge-syntax-highlight/package.json @@ -39,7 +39,12 @@ "scopeName": "source.bridge", "path": "./syntaxes/bridge.tmLanguage.json" } - ] + ], + "configurationDefaults": { + "[bridge]": { + "editor.defaultFormatter": "stackables.bridge-syntax-highlight" + } + } }, "scripts": { "prebuild": "pnpm --filter @stackables/bridge build", diff --git a/packages/bridge-syntax-highlight/src/server.ts b/packages/bridge-syntax-highlight/src/server.ts index 78324cb6..286dcf05 100644 --- a/packages/bridge-syntax-highlight/src/server.ts +++ b/packages/bridge-syntax-highlight/src/server.ts @@ -22,9 +22,11 @@ import { DiagnosticSeverity, CompletionItemKind, MarkupKind, + Range, + TextEdit, } from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { BridgeLanguageService } from "@stackables/bridge"; +import { BridgeLanguageService, parseBridgeCst, prettyPrintToSource } from "@stackables/bridge"; import type { CompletionKind } from "@stackables/bridge"; // ── Connection & document manager ────────────────────────────────────────── @@ -49,6 +51,7 @@ connection.onInitialize( capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, hoverProvider: true, + documentFormattingProvider: true, completionProvider: { triggerCharacters: ["."], }, @@ -131,6 +134,34 @@ connection.onCompletion((params) => { })); }); +connection.onDocumentFormatting((params) => { + const doc = documents.get(params.textDocument.uri); + if (!doc) return null; + + const text = doc.getText(); + + try { + parseBridgeCst(text); + } catch (error) { + connection.console.warn( + `Bridge formatting aborted due to syntax errors: ${String((error as Error)?.message ?? error)}`, + ); + return null; + } + + const formatted = prettyPrintToSource(text, { + tabSize: params.options.tabSize, + insertSpaces: params.options.insertSpaces, + }); + + const range = Range.create( + { line: 0, character: 0 }, + doc.positionAt(text.length), + ); + + return [TextEdit.replace(range, formatted)]; +}); + // ── Start ────────────────────────────────────────────────────────────────── documents.listen(connection); diff --git a/packages/bridge/test/bridge-printer.test.ts b/packages/bridge/test/bridge-printer.test.ts index b0c0cddc..95195748 100644 --- a/packages/bridge/test/bridge-printer.test.ts +++ b/packages/bridge/test/bridge-printer.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; -import { formatBridge } from "../src/index.ts"; +import { formatBridge, prettyPrintToSource } from "../src/index.ts"; /** * ============================================================================ @@ -221,6 +221,37 @@ describe("formatBridge - edge cases", () => { }); }); +describe("prettyPrintToSource - safety and options", () => { + test("is idempotent", () => { + const input = `version 1.5\nbridge Query.test {\nwith input as i\no.x<-i.y\n}`; + const once = prettyPrintToSource(input); + const twice = prettyPrintToSource(once); + assert.equal(twice, once); + }); + + test("throws on syntax errors", () => { + assert.throws(() => prettyPrintToSource(`bridge Query.test {`)); + }); + + test("uses tabSize when insertSpaces is true", () => { + const input = `version 1.5\nbridge Query.test {\nwith input as i\no.x<-i.y\n}`; + const expected = `version 1.5\n\nbridge Query.test {\n with input as i\n\n o.x <- i.y\n}\n`; + assert.equal( + prettyPrintToSource(input, { tabSize: 4, insertSpaces: true }), + expected, + ); + }); + + test("uses tabs when insertSpaces is false", () => { + const input = `version 1.5\nbridge Query.test {\nwith input as i\no.x<-i.y\n}`; + const expected = `version 1.5\n\nbridge Query.test {\n\twith input as i\n\n\to.x <- i.y\n}\n`; + assert.equal( + prettyPrintToSource(input, { tabSize: 4, insertSpaces: false }), + expected, + ); + }); +}); + describe("formatBridge - line splitting and joining", () => { test("content after '{' moves to new indented line", () => { const input = `bridge Query.greet { From 1bbc2b030685dff46364c2bd3ab5c8cdd8560962 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:58:05 +0000 Subject: [PATCH 3/4] Address review feedback for formatter API and LSP handler Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- packages/bridge-parser/src/bridge-printer.ts | 16 +++++++--- .../bridge-syntax-highlight/src/server.ts | 31 ++++++++++--------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/bridge-parser/src/bridge-printer.ts b/packages/bridge-parser/src/bridge-printer.ts index 6d275f17..1c545227 100644 --- a/packages/bridge-parser/src/bridge-printer.ts +++ b/packages/bridge-parser/src/bridge-printer.ts @@ -6,6 +6,7 @@ * from the token stream, applying consistent formatting rules. */ import type { IToken } from "chevrotain"; +import type { CstNode } from "chevrotain"; import { BridgeLexer } from "./parser/lexer.ts"; import { parseBridgeCst } from "./parser/parser.ts"; @@ -22,8 +23,10 @@ export type BridgeFormattingOptions = { function resolveFormattingOptions( options?: Partial, ): BridgeFormattingOptions { - const tabSize = Number.isInteger(options?.tabSize) - ? Math.max(1, options?.tabSize ?? DEFAULT_FORMATTING_OPTIONS.tabSize) + const rawTabSize = options?.tabSize; + const tabSize = + typeof rawTabSize === "number" && Number.isInteger(rawTabSize) + ? Math.max(1, rawTabSize) : DEFAULT_FORMATTING_OPTIONS.tabSize; return { @@ -167,11 +170,16 @@ function isTopLevelBlockStart(group: IToken[]): boolean { * @param source - The Bridge DSL source text to format * @returns Formatted source text, or the original if parsing fails */ +type PrettyPrintInput = string | { source: string; cst: CstNode }; + export function prettyPrintToSource( - source: string, + input: PrettyPrintInput, options?: Partial, ): string { - parseBridgeCst(source); + const source = typeof input === "string" ? input : input.source; + if (typeof input === "string") { + parseBridgeCst(source); + } return formatBridgeInternal(source, options); } diff --git a/packages/bridge-syntax-highlight/src/server.ts b/packages/bridge-syntax-highlight/src/server.ts index 286dcf05..42980279 100644 --- a/packages/bridge-syntax-highlight/src/server.ts +++ b/packages/bridge-syntax-highlight/src/server.ts @@ -141,25 +141,28 @@ connection.onDocumentFormatting((params) => { const text = doc.getText(); try { - parseBridgeCst(text); + const cst = parseBridgeCst(text); + const formatted = prettyPrintToSource( + { source: text, cst }, + { + tabSize: params.options.tabSize, + insertSpaces: params.options.insertSpaces, + }, + ); + + const range = Range.create( + { line: 0, character: 0 }, + doc.positionAt(text.length), + ); + + return [TextEdit.replace(range, formatted)]; } catch (error) { + const message = error instanceof Error ? error.message : String(error); connection.console.warn( - `Bridge formatting aborted due to syntax errors: ${String((error as Error)?.message ?? error)}`, + `Bridge formatting aborted due to syntax errors: ${message}`, ); return null; } - - const formatted = prettyPrintToSource(text, { - tabSize: params.options.tabSize, - insertSpaces: params.options.insertSpaces, - }); - - const range = Range.create( - { line: 0, character: 0 }, - doc.positionAt(text.length), - ); - - return [TextEdit.replace(range, formatted)]; }); // ── Start ────────────────────────────────────────────────────────────────── From 16bbf7054d16000f0cdf49c99c87702815213ce2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:16:36 +0000 Subject: [PATCH 4/4] Remove formatBridge compatibility API and switch playground/tests to strict formatter Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- packages/bridge-parser/src/bridge-printer.ts | 15 +---- packages/bridge-parser/src/index.ts | 1 - .../test/bridge-printer-examples.test.ts | 20 +++--- packages/bridge/test/bridge-printer.test.ts | 64 +++++++++---------- packages/bridge/test/formatter-test-utils.ts | 17 +++++ packages/playground/src/engine.ts | 4 +- packages/playground/src/usePlaygroundState.ts | 14 +++- 7 files changed, 73 insertions(+), 62 deletions(-) create mode 100644 packages/bridge/test/formatter-test-utils.ts diff --git a/packages/bridge-parser/src/bridge-printer.ts b/packages/bridge-parser/src/bridge-printer.ts index 1c545227..b8096496 100644 --- a/packages/bridge-parser/src/bridge-printer.ts +++ b/packages/bridge-parser/src/bridge-printer.ts @@ -167,8 +167,7 @@ function isTopLevelBlockStart(group: IToken[]): boolean { /** * Format Bridge DSL source code with consistent styling. * - * @param source - The Bridge DSL source text to format - * @returns Formatted source text, or the original if parsing fails + * Throws on syntax-invalid input when called with a source string. */ type PrettyPrintInput = string | { source: string; cst: CstNode }; @@ -524,15 +523,3 @@ function formatBridgeInternal( return result; } - -/** - * Backward-compatible formatter API. - * - * Returns the original source when syntax is invalid. - */ -export function formatBridge( - source: string, - options?: Partial, -): string { - return formatBridgeInternal(source, options); -} diff --git a/packages/bridge-parser/src/index.ts b/packages/bridge-parser/src/index.ts index df31269f..bf5fe06a 100644 --- a/packages/bridge-parser/src/index.ts +++ b/packages/bridge-parser/src/index.ts @@ -27,7 +27,6 @@ export { // ── Formatter ─────────────────────────────────────────────────────────────── export { - formatBridge, prettyPrintToSource, type BridgeFormattingOptions, } from "./bridge-printer.ts"; diff --git a/packages/bridge/test/bridge-printer-examples.test.ts b/packages/bridge/test/bridge-printer-examples.test.ts index 9c2c58e4..58007e83 100644 --- a/packages/bridge/test/bridge-printer-examples.test.ts +++ b/packages/bridge/test/bridge-printer-examples.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; -import { formatBridge } from "../src/index.ts"; +import { formatSnippet } from "./formatter-test-utils.ts"; /** * ============================================================================ @@ -19,7 +19,7 @@ tool geo from std.httpCall`; tool geo from std.httpCall `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("tool with body", () => { @@ -36,7 +36,7 @@ tool geo from std.httpCall { .method = GET } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("bridge block with assignments", () => { @@ -56,7 +56,7 @@ bridge Query.test { o.value <- i.value } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("define block", () => { @@ -70,7 +70,7 @@ o.x<-i.y o.x <- i.y } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("bridge with comment, tool handles, and pipes", () => { @@ -102,7 +102,7 @@ bridge Query.greet { o.lower <- lc:i.name } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("ternary expressions preserve formatting", () => { @@ -123,7 +123,7 @@ bridge Query.pricing { } `; // Should not change - assert.equal(formatBridge(input), input); + assert.equal(formatSnippet(input), input); }); test("blank line between top-level blocks", () => { @@ -158,14 +158,14 @@ define helper { with input as i } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("not operator preserves space", () => { const input = `o.requireMFA <- not i.verified `; // Should not change - assert.equal(formatBridge(input), input); + assert.equal(formatSnippet(input), input); }); test("blank lines between comments are preserved", () => { @@ -174,6 +174,6 @@ define helper { #sdasdsd `; // Should not change - assert.equal(formatBridge(input), input); + assert.equal(formatSnippet(input), input); }); }); diff --git a/packages/bridge/test/bridge-printer.test.ts b/packages/bridge/test/bridge-printer.test.ts index 95195748..65ed82e8 100644 --- a/packages/bridge/test/bridge-printer.test.ts +++ b/packages/bridge/test/bridge-printer.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; -import { formatBridge, prettyPrintToSource } from "../src/index.ts"; +import { prettyPrintToSource } from "../src/index.ts"; +import { formatSnippet } from "./formatter-test-utils.ts"; /** * ============================================================================ @@ -16,55 +17,55 @@ describe("formatBridge - spacing", () => { test("operator spacing: '<-' gets spaces", () => { const input = `o.x<-i.y`; const expected = `o.x <- i.y\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("operator spacing: '=' gets spaces", () => { const input = `.baseUrl="https://example.com"`; const expected = `.baseUrl = "https://example.com"\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("brace spacing: space before '{'", () => { const input = `bridge Query.test{`; const expected = `bridge Query.test {\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("no space after '.' in paths", () => { const input = `o.foo.bar`; const expected = `o.foo.bar\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("no space around '.' even with 'from' as property name", () => { const input = `c.from.station.id`; const expected = `c.from.station.id\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("'from' keyword gets spaces when used as keyword", () => { const input = `tool geo from std.httpCall`; const expected = `tool geo from std.httpCall\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("safe navigation '?.' has no spaces", () => { const input = `o.x?.y`; const expected = `o.x?.y\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("parentheses: no space inside", () => { const input = `foo( a , b )`; const expected = `foo(a, b)\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("brackets: no space inside", () => { const input = `arr[ 0 ]`; const expected = `arr[0]\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); }); @@ -80,7 +81,7 @@ o.x <- i.y o.x <- i.y } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("nested braces increase indentation", () => { @@ -95,7 +96,7 @@ on error { } } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); }); @@ -107,7 +108,7 @@ tool geo from std.httpCall`; tool geo from std.httpCall `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("preserve single blank line (user grouping)", () => { @@ -122,7 +123,7 @@ tool geo from std.httpCall o.x <- i.y } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("collapse multiple blank lines to one", () => { @@ -138,7 +139,7 @@ tool geo from std.httpCall o.x <- i.y } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("at least a single blank line between wires", () => { @@ -152,7 +153,7 @@ tool geo from std.httpCall o.x <- i.y } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); }); @@ -163,13 +164,13 @@ tool geo from std.httpCall`; const expected = `# This is a comment tool geo from std.httpCall `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("inline comment stays on same line", () => { const input = `tool geo from std.httpCall # inline`; const expected = `tool geo from std.httpCall # inline\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("trailing comment on brace line", () => { @@ -178,7 +179,7 @@ tool geo from std.httpCall const expected = `bridge Query.test { # comment } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); }); @@ -186,29 +187,28 @@ describe("formatBridge - on error blocks", () => { test("on error with simple value", () => { const input = `on error=null`; const expected = `on error = null\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("on error with JSON object stays on one line", () => { const input = `on error = { "connections": [] }`; const expected = `on error = {"connections": []}\n`; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); }); -describe("formatBridge - edge cases", () => { +describe("prettyPrintToSource - edge cases", () => { test("empty input", () => { - assert.equal(formatBridge(""), ""); + assert.equal(formatSnippet(""), ""); }); test("whitespace only input", () => { - assert.equal(formatBridge(" \n \n"), ""); + assert.equal(formatSnippet(" \n \n"), ""); }); - test("returns original on lexer errors", () => { + test("throws on lexer errors", () => { const invalid = `bridge @invalid { }`; - const output = formatBridge(invalid); - assert.ok(output.includes("@invalid")); + assert.throws(() => prettyPrintToSource(invalid)); }); test("comment-only file", () => { @@ -217,7 +217,7 @@ describe("formatBridge - edge cases", () => { const expected = `# comment 1 # comment 2 `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); }); @@ -270,7 +270,7 @@ describe("formatBridge - line splitting and joining", () => { } } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("standalone 'as', identifier, and '{' merge with previous line", () => { @@ -292,7 +292,7 @@ describe("formatBridge - line splitting and joining", () => { } } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("'as' in 'with' declaration is not merged incorrectly", () => { @@ -306,7 +306,7 @@ describe("formatBridge - line splitting and joining", () => { with output as o } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); test("adjacent blocks on same line get separated with blank line", () => { @@ -324,6 +324,6 @@ tool b from std.httpCall { .path = "/b" } `; - assert.equal(formatBridge(input), expected); + assert.equal(formatSnippet(input), expected); }); }); diff --git a/packages/bridge/test/formatter-test-utils.ts b/packages/bridge/test/formatter-test-utils.ts new file mode 100644 index 00000000..fd4aea1c --- /dev/null +++ b/packages/bridge/test/formatter-test-utils.ts @@ -0,0 +1,17 @@ +import { prettyPrintToSource } from "../src/index.ts"; + +/** + * Formatter unit tests include partial snippets (for spacing/line-shaping cases) + * that are intentionally not valid full Bridge documents. + * + * `prettyPrintToSource` supports a pre-validated CST input to skip strict parsing. + * The pretty-printer itself is token-based and does not read CST structure. + */ +type PrevalidatedInput = Exclude[0], string>; +// Intentionally a placeholder: formatter behavior under test is token-based and +// does not inspect CST contents when a pre-validated CST is provided. +const TEST_ONLY_PREVALIDATED_CST = {} as PrevalidatedInput["cst"]; + +export function formatSnippet(source: string): string { + return prettyPrintToSource({ source, cst: TEST_ONLY_PREVALIDATED_CST }); +} diff --git a/packages/playground/src/engine.ts b/packages/playground/src/engine.ts index f10a8c83..881ec416 100644 --- a/packages/playground/src/engine.ts +++ b/packages/playground/src/engine.ts @@ -8,9 +8,9 @@ import { parseBridgeChevrotain, parseBridgeDiagnostics, executeBridge, - formatBridge, + prettyPrintToSource, } from "@stackables/bridge"; -export { formatBridge }; +export { prettyPrintToSource }; import type { BridgeDiagnostic, Bridge, diff --git a/packages/playground/src/usePlaygroundState.ts b/packages/playground/src/usePlaygroundState.ts index 78437631..ac3070c2 100644 --- a/packages/playground/src/usePlaygroundState.ts +++ b/packages/playground/src/usePlaygroundState.ts @@ -9,7 +9,7 @@ import { extractOutputFields, extractInputSkeleton, mergeInputSkeleton, - formatBridge, + prettyPrintToSource, } from "./engine"; import type { RunResult } from "./engine"; import { buildSchema, type GraphQLSchema } from "graphql"; @@ -52,6 +52,14 @@ function tryFormatJson(val?: string): string { } } +function tryFormatBridge(source: string): string { + try { + return prettyPrintToSource(source); + } catch { + return source; + } +} + export function usePlaygroundState( initialExampleIndex = 0, forceStandalone = false, @@ -72,7 +80,7 @@ export function usePlaygroundState( // Format the default bridge if provided via overrides so it's not messy. const initialBridge = overrides?.bridge - ? formatBridge(overrides.bridge) + ? tryFormatBridge(overrides.bridge) : ex.bridge; const [schema, setSchema] = useState(ex.schema); const [bridge, setBridge] = useState(initialBridge); @@ -245,7 +253,7 @@ export function usePlaygroundState( }, [activeQuery, mode, schema, bridge, context]); const handleFormatBridge = useCallback(() => { - setBridge(formatBridge(bridge)); + setBridge(tryFormatBridge(bridge)); }, [bridge]); const diagnostics = getDiagnostics(bridge).diagnostics;