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:
@@ -15,9 +15,14 @@
|
||||
// the indent of the statement it precedes — same-line inline
|
||||
// positioning is intentionally NOT recovered; the goal is "verbatim
|
||||
// text", not "byte-exact placement".
|
||||
// * Blank lines between statements are preserved when the user wrote
|
||||
// them; multiples collapse to one. The formatter never *adds* blank
|
||||
// lines the user didn't write (rustfmt's default policy applied
|
||||
// strictly — no forced separation between top-level fn decls).
|
||||
// * Block bodies always use multi-line braces. `{}` for empty.
|
||||
// * If parse errors are reported by the parser, the formatter refuses
|
||||
// to emit anything and returns the first error.
|
||||
// to emit anything and returns the first error with line / column
|
||||
// coordinates (1-based, matching Rhai's own diagnostic format).
|
||||
|
||||
import type {
|
||||
BlockExpr,
|
||||
@@ -36,21 +41,47 @@ const PRINT_WIDTH = 100;
|
||||
|
||||
export type FormatResult =
|
||||
| { ok: true; text: string }
|
||||
| { ok: false; error: { message: string; offset: number } };
|
||||
| { ok: false; error: FormatError };
|
||||
|
||||
export interface FormatError {
|
||||
message: string;
|
||||
// 1-based line and column, matching Rhai's own diagnostic format.
|
||||
line: number;
|
||||
column: number;
|
||||
// Byte offset retained for callers that want to jump the editor
|
||||
// cursor (CodeMirror works in offsets, not line/col).
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export function format(source: string): FormatResult {
|
||||
const result = parse(source);
|
||||
if (result.errors.length > 0) {
|
||||
const first = result.errors[0];
|
||||
return { ok: false, error: errorPayload(first) };
|
||||
return { ok: false, error: errorPayload(source, result.errors[0]) };
|
||||
}
|
||||
const p = new Printer(result);
|
||||
p.printProgram();
|
||||
return { ok: true, text: p.finish() };
|
||||
}
|
||||
|
||||
function errorPayload(e: ParseError): { message: string; offset: number } {
|
||||
return { message: e.message, offset: e.start };
|
||||
function errorPayload(source: string, e: ParseError): FormatError {
|
||||
const { line, column } = lineColAt(source, e.start);
|
||||
return { message: e.message, line, column, offset: e.start };
|
||||
}
|
||||
|
||||
// Convert a byte offset into 1-based (line, column). Used for rendering
|
||||
// parser errors in a way that matches Rhai's own diagnostic format
|
||||
// (e.g. "Expecting name of a variable (line 2, position 4)").
|
||||
function lineColAt(source: string, offset: number): { line: number; column: number } {
|
||||
let line = 1;
|
||||
let lineStart = 0;
|
||||
const limit = Math.min(offset, source.length);
|
||||
for (let i = 0; i < limit; i++) {
|
||||
if (source.charCodeAt(i) === 10) {
|
||||
line++;
|
||||
lineStart = i + 1;
|
||||
}
|
||||
}
|
||||
return { line, column: limit - lineStart + 1 };
|
||||
}
|
||||
|
||||
class Printer {
|
||||
@@ -125,19 +156,27 @@ class Printer {
|
||||
|
||||
printProgram(): void {
|
||||
const stmts = this.result.program.stmts;
|
||||
let prevWasFn = false;
|
||||
for (let i = 0; i < stmts.length; i++) {
|
||||
const stmt = stmts[i];
|
||||
if (i > 0) {
|
||||
if (prevWasFn || stmt.kind === 'FnDecl') this.blankLine();
|
||||
if (this.hadBlankBetween(stmts[i - 1].end, stmt.start)) this.blankLine();
|
||||
else this.newline();
|
||||
}
|
||||
this.drainCommentsBefore(stmt.start);
|
||||
this.printStmt(stmt);
|
||||
prevWasFn = stmt.kind === 'FnDecl';
|
||||
}
|
||||
}
|
||||
|
||||
// "Did the user leave a blank line in this gap?" Consulted between
|
||||
// every pair of emitted statements to decide whether to keep the
|
||||
// vertical separator the source originally had.
|
||||
private hadBlankBetween(prevEnd: number, currStart: number): boolean {
|
||||
for (const offset of this.result.blankLines) {
|
||||
if (offset >= prevEnd && offset < currStart) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- statements
|
||||
|
||||
private printStmt(stmt: Stmt): void {
|
||||
@@ -231,7 +270,11 @@ class Printer {
|
||||
this.emit('{');
|
||||
this.indent++;
|
||||
for (let i = 0; i < block.stmts.length; i++) {
|
||||
this.newline();
|
||||
if (i > 0 && this.hadBlankBetween(block.stmts[i - 1].end, block.stmts[i].start)) {
|
||||
this.blankLine();
|
||||
} else {
|
||||
this.newline();
|
||||
}
|
||||
this.drainCommentsBefore(block.stmts[i].start);
|
||||
this.printStmt(block.stmts[i]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user