diff --git a/app/core/src/engine.ts b/app/core/src/engine.ts index 61b147a..a0d68a2 100644 --- a/app/core/src/engine.ts +++ b/app/core/src/engine.ts @@ -3,7 +3,7 @@ // a file. Diagrams are capped at 200 KB and the engine refuses ambiguity instead of searching, so a check is fast // (0.1–0.4 s for large diagrams). -import { compile, parse, resolve, THEMES } from "../../../engine/src/index.js" +import { compile, parse, resolve, SourceError, THEMES } from "../../../engine/src/index.js" export { parse, resolve } @@ -13,8 +13,8 @@ export function check(source: string): string | null { compile(source) return null } catch (e) { - const line = e && typeof (e as { line?: number }).line === "number" && (e as { line: number }).line > 0 ? `line ${(e as { line: number }).line}: ` : "" - return (line + String(e instanceof Error ? e.message : e)).slice(0, 2000) + // the engine's own "line N: …" (SourceError.format), as the app's editor shows it (ui/src/render.ts `refusal`) + return (e instanceof SourceError && e.line > 0 ? e.format() : String(e instanceof Error ? e.message : e)).slice(0, 2000) } } diff --git a/app/core/test/engine.test.ts b/app/core/test/engine.test.ts new file mode 100644 index 0000000..49e4246 --- /dev/null +++ b/app/core/test/engine.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest" +import { check } from "../src/engine.js" + +// check() is what a refused save, the scheduled drafts and Arch Agent's check_diagram report; the app's editor shows the +// same words (ui/src/render.ts `refusal`), so a line number reads the same everywhere (#32) +describe("check", () => { + it("is null when the source renders", () => { + expect(check('node a "A"\nnode b "B" right of a')).toBeNull() + }) + it("names the line of the mistake", () => { + expect(check('node a "A"\nnode b "B" rigth of a')).toBe('line 2: node "b": "rigth" is not a direction') + }) + it("names the line at fault in a statement written over several lines", () => { + expect(check('node a "A"\nnode b "B"\n style: rounded\n rigth of a')).toBe('line 4: node "b": "rigth" is not a direction') + }) +}) diff --git a/app/e2e/app.e2e.ts b/app/e2e/app.e2e.ts index f313977..42c1935 100644 --- a/app/e2e/app.e2e.ts +++ b/app/e2e/app.e2e.ts @@ -135,6 +135,19 @@ async function main() { await page.keyboard.press("Control+End") await page.keyboard.type('\nnode cache "Menu cache" below db style: store\n') await page.waitForFunction(() => document.querySelector("[data-card=system]")?.textContent?.includes("Menu cache")) + // a mistake: the status line and the card name its line, the gutter marks that line, Save waits (#32) + await page.keyboard.type('node oops "Oops" rigth of db') + await page.waitForFunction(() => /^line \d+: node "oops": "rigth" is not a direction$/.test(document.querySelector("[data-testid=status]")?.textContent?.trim() ?? "")) + const said = (await page.locator("[data-testid=status]").textContent())!.trim().match(/^line (\d+):/)![1] + const typedOn = String((await ta.inputValue()).split("\n").length) + const marked = (await page.locator("[data-error-line]").count()) ? (await page.locator("[data-error-line]").textContent())?.trim() : "no line" + if (said !== typedOn || marked !== typedOn) throw new Error(`the mistake is on line ${typedOn}; the status says ${said}, the gutter marks ${marked}`) + if (!(await page.locator("[data-card=system]").textContent())?.includes(`line ${said}: node "oops"`)) throw new Error("the card does not name the line") + if (await page.isEnabled("[data-testid=save]")) throw new Error("Save is enabled on a source that does not render") + await page.keyboard.press("Shift+Home") + await page.keyboard.press("Backspace") + await page.waitForFunction(() => document.querySelector("[data-testid=status]")?.textContent?.startsWith("Renders")) + if (await page.locator("[data-error-line]").count()) throw new Error("the gutter still marks a line after the mistake is gone") await page.keyboard.press("Control+s") // no git identity: the app asks who commits, then the save goes through await page.waitForSelector("[data-testid=identity-dialog]") diff --git a/app/ui/src/App.tsx b/app/ui/src/App.tsx index 4d68af6..636f5b1 100644 --- a/app/ui/src/App.tsx +++ b/app/ui/src/App.tsx @@ -785,7 +785,7 @@ export default function App() { ✕ - (setDrafts(d => ({ ...d, [sel]: v })), current?.sample && tourEvent("source.changed"))} /> + (setDrafts(d => ({ ...d, [sel]: v })), current?.sample && tourEvent("source.changed"))} />
{selCard?.r.error ?? (current?.source.kind === "github" ? "Renders. ⌘/Ctrl-S saves it as a change waiting for review; the card updates as you type." : "Renders. ⌘/Ctrl-S saves it; the card updates as you type.")}
@@ -1010,7 +1010,8 @@ function ProjectTitleDialog({ project, onClose, onSaved }: { project: Project; o } -function Editor({ value, onChange }: { value: string; onChange: (v: string) => void }) { +/** The source editor: a textarea with line numbers; the line the engine refused (`errorLine`) is marked in the gutter. */ +function Editor({ value, errorLine, onChange }: { value: string; errorLine?: number; onChange: (v: string) => void }) { const ta = useRef(null) const gutter = useRef(null) const lines = value.split("\n").length @@ -1018,7 +1019,9 @@ function Editor({ value, onChange }: { value: string; onChange: (v: string) => v
{Array.from({ length: lines }, (_, i) => ( -
{i + 1}
+
+ {i + 1} +
))}