fix(dashboard): preserve blank lines and improve Rhai parser errors

Two follow-ups on the Rhai formatter shipped in 0.5.1.

* Formatter no longer collapses user-intent blank lines between
  statements. The lexer now records a side-channel list of offsets
  where the source contained two-or-more consecutive newlines; the
  formatter consults it and emits a single blank in the same spot
  (rustfmt's `blank_lines_upper_bound = 1` policy applied strictly —
  the prior forced blank between top-level `fn` decls is dropped, so
  the formatter never *adds* a blank the user didn't write).
* Parse errors now read like Rhai's own diagnostics. `expect()` takes
  an optional `role` hint and each call site supplies a domain phrase
  (`name of a variable`, `function name in function declaration`,
  `'{' to begin a block`, `name of a property`, …). End-of-input is
  reported as `script is incomplete`. The dashboard banner renders
  `Parse error: {message} (line L, position C)` with 1-based
  coordinates, matching Rhai's format exactly.

The FormatError payload also keeps the byte `offset` so callers that
want to drive the editor cursor (CodeMirror works in offsets) still
have it.

Also folds the workspace Cargo.lock version bumps for 0.5.1 — the
lock-file rewrite that should have travelled with the prior commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-24 21:26:42 +02:00
parent 267c40f59c
commit 3d4c7b160b
8 changed files with 150 additions and 46 deletions

View File

@@ -22,11 +22,10 @@ describe('format — basic shape', () => {
);
});
it('separates top-level fn decls with a blank line', () => {
it('does not insert a blank between fn decls the user did not separate', () => {
// Strict preserve-only policy: no source blank => no emitted blank.
const out = formatted('fn a(){1}fn b(){2}');
expect(out).toBe(
'fn a() {\n\t1\n}\n\nfn b() {\n\t2\n}\n'
);
expect(out).toBe('fn a() {\n\t1\n}\nfn b() {\n\t2\n}\n');
});
it('renders if / else if / else with blocks', () => {
@@ -87,16 +86,48 @@ describe('format — reflow', () => {
});
});
describe('format — blank-line preservation', () => {
it('preserves a single blank line between statements', () => {
const src = 'let a = 1;\n\nlet b = 2;';
expect(formatted(src)).toBe('let a = 1;\n\nlet b = 2;\n');
});
it('collapses multiple blank lines to a single one', () => {
const src = 'let a = 1;\n\n\n\nlet b = 2;';
expect(formatted(src)).toBe('let a = 1;\n\nlet b = 2;\n');
});
it('preserves blanks inside block bodies', () => {
const src = 'fn process() {\n\tlet a = 1;\n\n\tlet b = 2;\n}';
expect(formatted(src)).toBe('fn process() {\n\tlet a = 1;\n\n\tlet b = 2;\n}\n');
});
it('does not invent blanks between adjacent statements', () => {
expect(formatted('let a=1;let b=2;')).toBe('let a = 1;\nlet b = 2;\n');
});
});
describe('format — parse failures', () => {
it('returns ok=false with the first parse error', () => {
const r = format('let = ;');
it('returns ok=false with a Rhai-flavored message and 1-based line/column', () => {
// Pattern from the user complaint: `let;` should surface as
// "Expecting name of a variable" at line/column.
const r = format('let msg = ctx.request.params.name;\nlet;\n');
expect(r.ok).toBe(false);
if (!r.ok) {
expect(typeof r.error.message).toBe('string');
expect(r.error.message).toBe('Expecting name of a variable');
expect(r.error.line).toBe(2);
expect(r.error.column).toBe(4);
expect(r.error.offset).toBeGreaterThanOrEqual(0);
}
});
it('reports script-incomplete on truncated input', () => {
// `fn` alone — the parser expects a function name and hits EOF.
const r = format('fn');
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error.message).toMatch(/script is incomplete/i);
});
it('does not partially rewrite when parsing fails', () => {
const r = format('let x = 1; this is garbage');
expect(r.ok).toBe(false);