@wildwinter/app-shell 0.52.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/history.ts"],"names":[],"mappings":"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","file":"history.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The file-byte history: undo and redo for writes an app makes to files, the\n// files-are-truth way. A step is a set of file-state transitions (a path's\n// content before and after, null meaning absent); undo writes the befores\n// back, redo the afters. No command log: the bytes on disk are the model, so\n// restoring bytes is undo.\n//\n// Storyletter's, lifted into the shell (patterkit design/patterpad-undo.md,\n// ruling 2) so Patterpad's project-level undo is the same code. The two apps'\n// DOCUMENT undo still shares nothing (Patterpad's open scene is ProseMirror's);\n// this is the file half only.\n//\n// Three things beyond Storyletter's first version:\n//\n// - DRIFT IS REFUSED (ruling 5). Before an undo or redo, each file must still\n// say what the step left it saying (its after, for an undo; its before, for a\n// redo). A file changed since, by a pull, a merge or another app, would have\n// that work overwritten, so the step is refused, naming the file, and dropped\n// with every step below it: none of them can now be applied safely.\n// - A PER-FILE RESTORE HOOK, for a file an app restores its own way (Patterpad\n// restores a `.patterx` block by block but stamps its edit trail afresh, which\n// moves on every save and so must not count as drift).\n// - THE WRITER IS THE APP'S. Patterpad writes through its own ordered queue;\n// `applyStates` (all or nothing through the VC layer) is the default.\n//\n// Main-process code: it reads and writes files.\n// ---------------------------------------------------------------------------\n\nimport { existsSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { deleteFileAsync, writeTextFilesAsync } from \"@wildwinter/simple-vc-lib\";\nimport type { VCWriteOutcome } from \"@wildwinter/simple-vc-lib\";\n\n/** A file's content, or null when the file is absent (created / deleted). */\nexport interface FileState {\n path: string;\n content: string | null;\n /** Bookkeeping that lives beside the project but outside version control: restored with plain writes,\n * never checked out or added, once the versioned files have landed. */\n local?: boolean;\n}\n\n/** Read the current on-disk state of a set of paths. */\nexport function captureBefore(paths: string[]): FileState[] {\n return paths.map((path) => ({ path, content: existsSync(path) ? readFileSync(path, \"utf8\") : null }));\n}\n\n/** What applying a set of states came to. `wrote` says whether the writes landed before something else (a\n * delete) failed, which decides whether there is now a change on disk for the history to know about. */\nexport type Applied = { ok: true } | { ok: false; error: string; wrote: boolean };\n\n/** How a set of states is written: `applyStates`, or the app's own write path. */\nexport type StateWriter = (states: FileState[]) => Promise<Applied>;\n\n/**\n * Apply file states through the VC layer: write content, or delete when null.\n *\n * ALL OR NOTHING, and off the main thread (simple-vc-lib 0.5.0): every file is checked out first, and if any\n * one is refused nothing is written. The deletes run only once the whole batch has landed, so a rename whose\n * new path was refused never loses the old file.\n */\nexport async function applyStates(all: FileState[]): Promise<Applied> {\n const states = all.filter((s) => s.local !== true);\n const writes = states.filter((s) => s.content !== null).map((s) => ({ filePath: s.path, content: s.content! }));\n if (writes.length > 0) {\n try {\n const batch = await writeTextFilesAsync(writes, \"utf8\", { allOrNothing: true });\n if (!batch.success) return { ok: false, error: writeFailure(batch.results), wrote: false };\n } catch (e) {\n return { ok: false, error: `couldn't save: ${e instanceof Error ? e.message : String(e)}`, wrote: false };\n }\n }\n const undeleted: string[] = [];\n for (const s of states) {\n if (s.content !== null || !existsSync(s.path)) continue;\n try {\n const gone = await deleteFileAsync(s.path);\n if (!gone.success) undeleted.push(`${basename(s.path)} (${gone.message || gone.status})`);\n } catch (e) {\n undeleted.push(`${basename(s.path)} (${e instanceof Error ? e.message : String(e)})`);\n }\n }\n if (undeleted.length > 0) return { ok: false, error: `couldn't delete ${undeleted.join(\"; \")}`, wrote: writes.length > 0 };\n // The bookkeeping last, and only once the project's own files have landed, so it never says the project is\n // at a revision its files are not.\n for (const s of all) {\n if (s.local !== true) continue;\n if (s.content === null) rmSync(s.path, { force: true });\n else writeFileSync(s.path, s.content, \"utf8\");\n }\n return { ok: true };\n}\n\n/**\n * What a refused batch says: the file, and why. A refusal names who holds it (\"'docks.storyletdeck' is locked\n * by bob@bob-ws\"); every other file in an all-or-nothing batch reports only that it was not prepared because of\n * that one, which is not worth repeating.\n */\nexport function writeFailure(results: VCWriteOutcome[]): string {\n const failed = results.filter((r) => !r.success);\n const refused = failed.filter((r) => r.status === \"locked\" || r.status === \"outOfDate\");\n const say = (r: VCWriteOutcome): string => {\n const name = basename(r.filePath);\n const why = r.message || r.status;\n return why.includes(name) ? why : `${name} (${why})`;\n };\n if (refused.length > 0) return `nothing was saved: ${refused.map(say).join(\"; \")}`;\n return `couldn't save ${failed.map(say).join(\"; \") || \"the project\"}`;\n}\n\n/** How many steps Undo keeps: ProseMirror's default depth, the same in both apps. A step holds whole files\n * before and after, so an uncapped stack grew for as long as the window stayed open. */\nexport const UNDO_LIMIT = 100;\n\n/**\n * A file an app restores its own way. `same` says whether the file on disk still says what the step expects\n * (the drift check), and `restore` gives the content to write for the target, from what is on disk now. A\n * `.patterx` compares and restores every block but its edit trail, which the undo stamps afresh.\n */\nexport interface FileRestorer {\n same(expected: string | null, current: string | null): boolean;\n restore(target: string | null, current: string | null): string | null;\n}\n\nexport interface HistoryOptions {\n /** How an undo or redo is written. Default `applyStates`. */\n write?: StateWriter;\n /** A restorer for a file the app restores its own way, by path; undefined for every other file. */\n restorer?: (path: string) => FileRestorer | undefined;\n /** How each file's current bytes are read for the drift check. Default from disk (`captureBefore`). */\n read?: (paths: string[]) => FileState[];\n}\n\n/** What an undo or redo came to. `drift` names the files that changed since the step, when that refused it. */\nexport type StepOutcome =\n | { ok: true; label: string }\n | { ok: false; label: string; error: string; drift?: string[] };\n\ninterface Entry {\n label: string;\n key: string;\n before: FileState[];\n after: FileState[];\n}\n\n/** Merge state lists by path, `override` winning. */\nfunction mergeStates(base: FileState[], override: FileState[]): FileState[] {\n const byPath = new Map<string, FileState>();\n for (const s of base) byPath.set(s.path, s);\n for (const s of override) byPath.set(s.path, s);\n return [...byPath.values()];\n}\n\n/** File names, as a refusal lists them. */\nconst listNames = (paths: string[]): string => paths.map((p) => basename(p)).join(\", \");\n\nexport class History {\n private undoStack: Entry[] = [];\n private redoStack: Entry[] = [];\n private readonly opts: HistoryOptions;\n\n constructor(opts: HistoryOptions = {}) {\n this.opts = opts;\n }\n\n reset(): void {\n this.undoStack = [];\n this.redoStack = [];\n }\n\n get canUndo(): boolean { return this.undoStack.length > 0; }\n get canRedo(): boolean { return this.redoStack.length > 0; }\n /** The labels of the next undo and redo, for a menu (\"Undo Rename Card\"). */\n get undoLabel(): string | undefined { return this.undoStack[this.undoStack.length - 1]?.label; }\n get redoLabel(): string | undefined { return this.redoStack[this.redoStack.length - 1]?.label; }\n\n /** Record a committed change; coalesce with the top entry when the key matches (the same logical edit\n * continuing, as typing into one field). A new distinct edit clears redo. */\n record(label: string, key: string, before: FileState[], after: FileState[]): void {\n const top = this.undoStack[this.undoStack.length - 1];\n if (top && top.key === key) {\n top.before = mergeStates(before, top.before); // keep the earliest before per path\n top.after = mergeStates(top.after, after); // keep the latest after per path\n } else {\n this.undoStack.push({ label, key, before, after });\n if (this.undoStack.length > UNDO_LIMIT) this.undoStack.shift();\n }\n this.redoStack = [];\n }\n\n /**\n * Fold a write the app made on its own into the step the author took last, rather than recording a step of\n * its own: for a write nobody asked for (a value stored the first time a view is drawn), which a step of its\n * own would have the author's next Undo undo instead of what they meant. With nothing to undo yet, there is\n * nothing to fold into, and the write simply stands.\n */\n fold(before: FileState[], after: FileState[]): void {\n const top = this.undoStack[this.undoStack.length - 1];\n if (!top) return;\n const known = new Set(top.before.map((s) => s.path));\n top.before = [...top.before, ...before.filter((s) => !known.has(s.path))];\n top.after = mergeStates(top.after, after);\n }\n\n /** Move the top undo entry to redo and return its before-states, for a caller that writes them itself (and\n * checks drift itself). Most callers want `applyUndo`. */\n undo(): FileState[] | undefined {\n const entry = this.undoStack.pop();\n if (!entry) return undefined;\n this.redoStack.push(entry);\n return entry.before;\n }\n\n /** Move the top redo entry to undo and return its after-states (see `undo`). */\n redo(): FileState[] | undefined {\n const entry = this.redoStack.pop();\n if (!entry) return undefined;\n this.undoStack.push(entry);\n return entry.after;\n }\n\n /** An undo whose states could not be written: the step goes back where it was, so the author can try again\n * once the file can be written. */\n undoFailed(): void {\n const entry = this.redoStack.pop();\n if (entry) this.undoStack.push(entry);\n }\n\n /** The same for a redo that could not be written. */\n redoFailed(): void {\n const entry = this.undoStack.pop();\n if (entry) this.redoStack.push(entry);\n }\n\n /**\n * Undo the top step: check every file still says what the step left it saying, then write its befores. A\n * file that has changed since refuses the undo, and the step is dropped with every step below it (they were\n * taken on top of what has now been overwritten). A write that fails puts the step back. Undefined when there\n * is nothing to undo.\n */\n async applyUndo(): Promise<StepOutcome | undefined> {\n return this.apply(\"undo\");\n }\n\n /** Redo the top undone step, by the same rules: drift drops it and every step above it on the redo stack. */\n async applyRedo(): Promise<StepOutcome | undefined> {\n return this.apply(\"redo\");\n }\n\n private async apply(way: \"undo\" | \"redo\"): Promise<StepOutcome | undefined> {\n const from = way === \"undo\" ? this.undoStack : this.redoStack;\n const entry = from[from.length - 1];\n if (!entry) return undefined;\n const expected = way === \"undo\" ? entry.after : entry.before;\n const target = way === \"undo\" ? entry.before : entry.after;\n const restorer = this.opts.restorer;\n const current = new Map((this.opts.read ?? captureBefore)(target.map((s) => s.path)).map((s) => [s.path, s.content]));\n\n // The drift check, over every file the step touches.\n const drifted = expected.filter((s) => {\n if (s.local === true) return false; // bookkeeping outside version control is the app's own\n const now = current.get(s.path) ?? null;\n const r = restorer?.(s.path);\n return r ? !r.same(s.content, now) : now !== s.content;\n }).map((s) => s.path);\n if (drifted.length > 0) {\n // Every step from here down was taken on top of what has changed: none can be applied safely.\n from.length = 0;\n const name = drifted.length === 1 ? `${basename(drifted[0]!)} has` : `${listNames(drifted)} have`;\n return {\n ok: false, label: entry.label, drift: drifted,\n error: `couldn't ${way} \"${entry.label}\": ${name} changed since, so ${way === \"undo\" ? \"undoing\" : \"redoing\"} it would overwrite that. `\n + `Nothing from that step ${way === \"undo\" ? \"back\" : \"on\"} can be ${way === \"undo\" ? \"undone\" : \"redone\"} now.`,\n };\n }\n\n const states = target.map((s) => {\n const r = restorer?.(s.path);\n return r ? { ...s, content: r.restore(s.content, current.get(s.path) ?? null) } : s;\n });\n from.pop();\n (way === \"undo\" ? this.redoStack : this.undoStack).push(entry);\n const applied = await (this.opts.write ?? applyStates)(states);\n if (!applied.ok) {\n if (way === \"undo\") this.undoFailed(); else this.redoFailed();\n return { ok: false, label: entry.label, error: applied.error };\n }\n return { ok: true, label: entry.label };\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/history.ts"],"names":[],"mappings":";;;;;AA2CO,SAAS,cAAc,KAAA,EAA8B;AAC1D,EAAA,OAAO,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,MAAU,EAAE,IAAA,EAAM,OAAA,EAAS,UAAA,CAAW,IAAI,IAAI,YAAA,CAAa,IAAA,EAAM,MAAM,CAAA,GAAI,MAAK,CAAE,CAAA;AACtG;AAgBA,eAAsB,YAAY,GAAA,EAAoC;AACpE,EAAA,MAAM,SAAS,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,UAAU,IAAI,CAAA;AACjD,EAAA,MAAM,SAAS,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,YAAY,IAAI,CAAA,CAAE,IAAI,CAAC,CAAA,MAAO,EAAE,QAAA,EAAU,CAAA,CAAE,MAAM,OAAA,EAAS,CAAA,CAAE,SAAS,CAAE,CAAA;AAC9G,EAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG;AACrB,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,MAAM,mBAAA,CAAoB,MAAA,EAAQ,QAAQ,EAAE,YAAA,EAAc,MAAM,CAAA;AAC9E,MAAA,IAAI,CAAC,KAAA,CAAM,OAAA,EAAS,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,YAAA,CAAa,KAAA,CAAM,OAAO,CAAA,EAAG,OAAO,KAAA,EAAM;AAAA,IAC3F,SAAS,CAAA,EAAG;AACV,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,kBAAkB,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAA,EAAI,OAAO,KAAA,EAAM;AAAA,IAC1G;AAAA,EACF;AACA,EAAA,MAAM,YAAsB,EAAC;AAC7B,EAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AACtB,IAAA,IAAI,EAAE,OAAA,KAAY,IAAA,IAAQ,CAAC,UAAA,CAAW,CAAA,CAAE,IAAI,CAAA,EAAG;AAC/C,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAM,eAAA,CAAgB,CAAA,CAAE,IAAI,CAAA;AACzC,MAAA,IAAI,CAAC,IAAA,CAAK,OAAA,EAAS,SAAA,CAAU,KAAK,CAAA,EAAG,QAAA,CAAS,CAAA,CAAE,IAAI,CAAC,CAAA,EAAA,EAAK,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,IAC1F,SAAS,CAAA,EAAG;AACV,MAAA,SAAA,CAAU,IAAA,CAAK,CAAA,EAAG,QAAA,CAAS,CAAA,CAAE,IAAI,CAAC,CAAA,EAAA,EAAK,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,IACtF;AAAA,EACF;AACA,EAAA,IAAI,UAAU,MAAA,GAAS,CAAA,EAAG,OAAO,EAAE,IAAI,KAAA,EAAO,KAAA,EAAO,CAAA,gBAAA,EAAmB,SAAA,CAAU,KAAK,IAAI,CAAC,IAAI,KAAA,EAAO,MAAA,CAAO,SAAS,CAAA,EAAE;AAGzH,EAAA,KAAA,MAAW,KAAK,GAAA,EAAK;AACnB,IAAA,IAAI,CAAA,CAAE,UAAU,IAAA,EAAM;AACtB,IAAA,IAAI,CAAA,CAAE,YAAY,IAAA,EAAM,MAAA,CAAO,EAAE,IAAA,EAAM,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA,SACjD,aAAA,CAAc,CAAA,CAAE,IAAA,EAAM,CAAA,CAAE,SAAS,MAAM,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,EAAE,IAAI,IAAA,EAAK;AACpB;AAOO,SAAS,aAAa,OAAA,EAAmC;AAC9D,EAAA,MAAM,SAAS,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,EAAE,OAAO,CAAA;AAC/C,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,KAAM,EAAE,MAAA,KAAW,QAAA,IAAY,CAAA,CAAE,MAAA,KAAW,WAAW,CAAA;AACtF,EAAA,MAAM,GAAA,GAAM,CAAC,CAAA,KAA8B;AACzC,IAAA,MAAM,IAAA,GAAO,QAAA,CAAS,CAAA,CAAE,QAAQ,CAAA;AAChC,IAAA,MAAM,GAAA,GAAM,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,MAAA;AAC3B,IAAA,OAAO,GAAA,CAAI,SAAS,IAAI,CAAA,GAAI,MAAM,CAAA,EAAG,IAAI,KAAK,GAAG,CAAA,CAAA,CAAA;AAAA,EACnD,CAAA;AACA,EAAA,IAAI,OAAA,CAAQ,MAAA,GAAS,CAAA,EAAG,OAAO,CAAA,mBAAA,EAAsB,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAChF,EAAA,OAAO,CAAA,cAAA,EAAiB,OAAO,GAAA,CAAI,GAAG,EAAE,IAAA,CAAK,IAAI,KAAK,aAAa,CAAA,CAAA;AACrE;AAIO,IAAM,UAAA,GAAa;AAkC1B,SAAS,WAAA,CAAY,MAAmB,QAAA,EAAoC;AAC1E,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAuB;AAC1C,EAAA,KAAA,MAAW,KAAK,IAAA,EAAM,MAAA,CAAO,GAAA,CAAI,CAAA,CAAE,MAAM,CAAC,CAAA;AAC1C,EAAA,KAAA,MAAW,KAAK,QAAA,EAAU,MAAA,CAAO,GAAA,CAAI,CAAA,CAAE,MAAM,CAAC,CAAA;AAC9C,EAAA,OAAO,CAAC,GAAG,MAAA,CAAO,MAAA,EAAQ,CAAA;AAC5B;AAGA,IAAM,SAAA,GAAY,CAAC,KAAA,KAA4B,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,QAAA,CAAS,CAAC,CAAC,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AAE/E,IAAM,UAAN,MAAc;AAAA,EACX,YAAqB,EAAC;AAAA,EACtB,YAAqB,EAAC;AAAA,EACb,IAAA;AAAA,EAEjB,WAAA,CAAY,IAAA,GAAuB,EAAC,EAAG;AACrC,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACd;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,YAAY,EAAC;AAClB,IAAA,IAAA,CAAK,YAAY,EAAC;AAAA,EACpB;AAAA,EAEA,IAAI,OAAA,GAAmB;AAAE,IAAA,OAAO,IAAA,CAAK,UAAU,MAAA,GAAS,CAAA;AAAA,EAAG;AAAA,EAC3D,IAAI,OAAA,GAAmB;AAAE,IAAA,OAAO,IAAA,CAAK,UAAU,MAAA,GAAS,CAAA;AAAA,EAAG;AAAA;AAAA,EAE3D,IAAI,SAAA,GAAgC;AAAE,IAAA,OAAO,KAAK,SAAA,CAAU,IAAA,CAAK,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA,EAAG,KAAA;AAAA,EAAO;AAAA,EAC/F,IAAI,SAAA,GAAgC;AAAE,IAAA,OAAO,KAAK,SAAA,CAAU,IAAA,CAAK,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA,EAAG,KAAA;AAAA,EAAO;AAAA;AAAA;AAAA,EAI/F,MAAA,CAAO,KAAA,EAAe,GAAA,EAAa,MAAA,EAAqB,KAAA,EAA0B;AAChF,IAAA,MAAM,MAAM,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,SAAA,CAAU,SAAS,CAAC,CAAA;AACpD,IAAA,IAAI,GAAA,IAAO,GAAA,CAAI,GAAA,KAAQ,GAAA,EAAK;AAC1B,MAAA,GAAA,CAAI,MAAA,GAAS,WAAA,CAAY,MAAA,EAAQ,GAAA,CAAI,MAAM,CAAA;AAC3C,MAAA,GAAA,CAAI,KAAA,GAAQ,WAAA,CAAY,GAAA,CAAI,KAAA,EAAO,KAAK,CAAA;AAAA,IAC1C,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,UAAU,IAAA,CAAK,EAAE,OAAO,GAAA,EAAK,MAAA,EAAQ,OAAO,CAAA;AACjD,MAAA,IAAI,KAAK,SAAA,CAAU,MAAA,GAAS,UAAA,EAAY,IAAA,CAAK,UAAU,KAAA,EAAM;AAAA,IAC/D;AACA,IAAA,IAAA,CAAK,YAAY,EAAC;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,IAAA,CAAK,QAAqB,KAAA,EAA0B;AAClD,IAAA,MAAM,MAAM,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,SAAA,CAAU,SAAS,CAAC,CAAA;AACpD,IAAA,IAAI,CAAC,GAAA,EAAK;AACV,IAAA,MAAM,KAAA,GAAQ,IAAI,GAAA,CAAI,GAAA,CAAI,MAAA,CAAO,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC,CAAA;AACnD,IAAA,GAAA,CAAI,SAAS,CAAC,GAAG,GAAA,CAAI,MAAA,EAAQ,GAAG,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,KAAA,CAAM,GAAA,CAAI,CAAA,CAAE,IAAI,CAAC,CAAC,CAAA;AACxE,IAAA,GAAA,CAAI,KAAA,GAAQ,WAAA,CAAY,GAAA,CAAI,KAAA,EAAO,KAAK,CAAA;AAAA,EAC1C;AAAA;AAAA;AAAA,EAIA,IAAA,GAAgC;AAC9B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,SAAA,CAAU,GAAA,EAAI;AACjC,IAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,IAAA,IAAA,CAAK,SAAA,CAAU,KAAK,KAAK,CAAA;AACzB,IAAA,OAAO,KAAA,CAAM,MAAA;AAAA,EACf;AAAA;AAAA,EAGA,IAAA,GAAgC;AAC9B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,SAAA,CAAU,GAAA,EAAI;AACjC,IAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,IAAA,IAAA,CAAK,SAAA,CAAU,KAAK,KAAK,CAAA;AACzB,IAAA,OAAO,KAAA,CAAM,KAAA;AAAA,EACf;AAAA;AAAA;AAAA,EAIA,SAAA,GAAkB;AAChB,IAAA,IAAA,CAAK,YAAY,EAAC;AAAA,EACpB;AAAA;AAAA;AAAA,EAIA,UAAA,GAAmB;AACjB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,SAAA,CAAU,GAAA,EAAI;AACjC,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,KAAK,CAAA;AAAA,EACtC;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,SAAA,CAAU,GAAA,EAAI;AACjC,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,KAAK,CAAA;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAA,GAA8C;AAClD,IAAA,OAAO,IAAA,CAAK,MAAM,MAAM,CAAA;AAAA,EAC1B;AAAA;AAAA,EAGA,MAAM,SAAA,GAA8C;AAClD,IAAA,OAAO,IAAA,CAAK,MAAM,MAAM,CAAA;AAAA,EAC1B;AAAA,EAEA,MAAc,MAAM,GAAA,EAAwD;AAC1E,IAAA,MAAM,IAAA,GAAO,GAAA,KAAQ,MAAA,GAAS,IAAA,CAAK,YAAY,IAAA,CAAK,SAAA;AACpD,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,CAAK,MAAA,GAAS,CAAC,CAAA;AAClC,IAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,IAAA,MAAM,QAAA,GAAW,GAAA,KAAQ,MAAA,GAAS,KAAA,CAAM,QAAQ,KAAA,CAAM,MAAA;AACtD,IAAA,MAAM,MAAA,GAAS,GAAA,KAAQ,MAAA,GAAS,KAAA,CAAM,SAAS,KAAA,CAAM,KAAA;AACrD,IAAA,MAAM,QAAA,GAAW,KAAK,IAAA,CAAK,QAAA;AAC3B,IAAA,MAAM,OAAA,GAAU,IAAI,GAAA,CAAA,CAAK,IAAA,CAAK,IAAA,CAAK,QAAQ,aAAA,EAAe,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,EAAE,IAAA,EAAM,CAAA,CAAE,OAAO,CAAC,CAAC,CAAA;AAGpH,IAAA,MAAM,OAAA,GAAU,QAAA,CAAS,MAAA,CAAO,CAAC,CAAA,KAAM;AACrC,MAAA,IAAI,CAAA,CAAE,KAAA,KAAU,IAAA,EAAM,OAAO,KAAA;AAC7B,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,CAAI,CAAA,CAAE,IAAI,CAAA,IAAK,IAAA;AACnC,MAAA,MAAM,CAAA,GAAI,QAAA,GAAW,CAAA,CAAE,IAAI,CAAA;AAC3B,MAAA,OAAO,CAAA,GAAI,CAAC,CAAA,CAAE,IAAA,CAAK,EAAE,OAAA,EAAS,GAAG,CAAA,GAAI,GAAA,KAAQ,CAAA,CAAE,OAAA;AAAA,IACjD,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AACpB,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AAEtB,MAAA,IAAA,CAAK,MAAA,GAAS,CAAA;AACd,MAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,KAAW,CAAA,GAAI,GAAG,QAAA,CAAS,OAAA,CAAQ,CAAC,CAAE,CAAC,CAAA,IAAA,CAAA,GAAS,CAAA,EAAG,SAAA,CAAU,OAAO,CAAC,CAAA,KAAA,CAAA;AAC1F,MAAA,OAAO;AAAA,QACL,EAAA,EAAI,KAAA;AAAA,QAAO,OAAO,KAAA,CAAM,KAAA;AAAA,QAAO,KAAA,EAAO,OAAA;AAAA,QACtC,KAAA,EAAO,YAAY,GAAG,CAAA,EAAA,EAAK,MAAM,KAAK,CAAA,GAAA,EAAM,IAAI,CAAA,mBAAA,EAAsB,GAAA,KAAQ,MAAA,GAAS,YAAY,SAAS,CAAA,iDAAA,EAC9E,QAAQ,MAAA,GAAS,MAAA,GAAS,IAAI,CAAA,QAAA,EAAW,GAAA,KAAQ,MAAA,GAAS,QAAA,GAAW,QAAQ,CAAA,KAAA;AAAA,OAC7G;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM;AAC/B,MAAA,MAAM,CAAA,GAAI,QAAA,GAAW,CAAA,CAAE,IAAI,CAAA;AAC3B,MAAA,OAAO,IAAI,EAAE,GAAG,CAAA,EAAG,OAAA,EAAS,EAAE,OAAA,CAAQ,CAAA,CAAE,OAAA,EAAS,OAAA,CAAQ,IAAI,CAAA,CAAE,IAAI,CAAA,IAAK,IAAI,GAAE,GAAI,CAAA;AAAA,IACpF,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,GAAA,EAAI;AACT,IAAA,CAAC,QAAQ,MAAA,GAAS,IAAA,CAAK,YAAY,IAAA,CAAK,SAAA,EAAW,KAAK,KAAK,CAAA;AAC7D,IAAA,MAAM,UAAU,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,KAAA,IAAS,aAAa,MAAM,CAAA;AAC7D,IAAA,IAAI,CAAC,QAAQ,EAAA,EAAI;AACf,MAAA,IAAI,GAAA,KAAQ,MAAA,EAAQ,IAAA,CAAK,UAAA,EAAW;AAAA,gBAAa,UAAA,EAAW;AAC5D,MAAA,OAAO,EAAE,IAAI,KAAA,EAAO,KAAA,EAAO,MAAM,KAAA,EAAO,KAAA,EAAO,QAAQ,KAAA,EAAM;AAAA,IAC/D;AACA,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,MAAM,KAAA,EAAM;AAAA,EACxC;AACF","file":"history.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The file-byte history: undo and redo for writes an app makes to files, the\n// files-are-truth way. A step is a set of file-state transitions (a path's\n// content before and after, null meaning absent); undo writes the befores\n// back, redo the afters. No command log: the bytes on disk are the model, so\n// restoring bytes is undo.\n//\n// Storyletter's, lifted into the shell (patterkit design/patterpad-undo.md,\n// ruling 2) so Patterpad's project-level undo is the same code. The two apps'\n// DOCUMENT undo still shares nothing (Patterpad's open scene is ProseMirror's);\n// this is the file half only.\n//\n// Three things beyond Storyletter's first version:\n//\n// - DRIFT IS REFUSED (ruling 5). Before an undo or redo, each file must still\n// say what the step left it saying (its after, for an undo; its before, for a\n// redo). A file changed since, by a pull, a merge or another app, would have\n// that work overwritten, so the step is refused, naming the file, and dropped\n// with every step below it: none of them can now be applied safely.\n// - A PER-FILE RESTORE HOOK, for a file an app restores its own way (Patterpad\n// restores a `.patterx` block by block but stamps its edit trail afresh, which\n// moves on every save and so must not count as drift).\n// - THE WRITER IS THE APP'S. Patterpad writes through its own ordered queue;\n// `applyStates` (all or nothing through the VC layer) is the default.\n//\n// Main-process code: it reads and writes files.\n// ---------------------------------------------------------------------------\n\nimport { existsSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { deleteFileAsync, writeTextFilesAsync } from \"@wildwinter/simple-vc-lib\";\nimport type { VCWriteOutcome } from \"@wildwinter/simple-vc-lib\";\n\n/** A file's content, or null when the file is absent (created / deleted). */\nexport interface FileState {\n path: string;\n content: string | null;\n /** Bookkeeping that lives beside the project but outside version control: restored with plain writes,\n * never checked out or added, once the versioned files have landed. */\n local?: boolean;\n}\n\n/** Read the current on-disk state of a set of paths. */\nexport function captureBefore(paths: string[]): FileState[] {\n return paths.map((path) => ({ path, content: existsSync(path) ? readFileSync(path, \"utf8\") : null }));\n}\n\n/** What applying a set of states came to. `wrote` says whether the writes landed before something else (a\n * delete) failed, which decides whether there is now a change on disk for the history to know about. */\nexport type Applied = { ok: true } | { ok: false; error: string; wrote: boolean };\n\n/** How a set of states is written: `applyStates`, or the app's own write path. */\nexport type StateWriter = (states: FileState[]) => Promise<Applied>;\n\n/**\n * Apply file states through the VC layer: write content, or delete when null.\n *\n * ALL OR NOTHING, and off the main thread (simple-vc-lib 0.5.0): every file is checked out first, and if any\n * one is refused nothing is written. The deletes run only once the whole batch has landed, so a rename whose\n * new path was refused never loses the old file.\n */\nexport async function applyStates(all: FileState[]): Promise<Applied> {\n const states = all.filter((s) => s.local !== true);\n const writes = states.filter((s) => s.content !== null).map((s) => ({ filePath: s.path, content: s.content! }));\n if (writes.length > 0) {\n try {\n const batch = await writeTextFilesAsync(writes, \"utf8\", { allOrNothing: true });\n if (!batch.success) return { ok: false, error: writeFailure(batch.results), wrote: false };\n } catch (e) {\n return { ok: false, error: `couldn't save: ${e instanceof Error ? e.message : String(e)}`, wrote: false };\n }\n }\n const undeleted: string[] = [];\n for (const s of states) {\n if (s.content !== null || !existsSync(s.path)) continue;\n try {\n const gone = await deleteFileAsync(s.path);\n if (!gone.success) undeleted.push(`${basename(s.path)} (${gone.message || gone.status})`);\n } catch (e) {\n undeleted.push(`${basename(s.path)} (${e instanceof Error ? e.message : String(e)})`);\n }\n }\n if (undeleted.length > 0) return { ok: false, error: `couldn't delete ${undeleted.join(\"; \")}`, wrote: writes.length > 0 };\n // The bookkeeping last, and only once the project's own files have landed, so it never says the project is\n // at a revision its files are not.\n for (const s of all) {\n if (s.local !== true) continue;\n if (s.content === null) rmSync(s.path, { force: true });\n else writeFileSync(s.path, s.content, \"utf8\");\n }\n return { ok: true };\n}\n\n/**\n * What a refused batch says: the file, and why. A refusal names who holds it (\"'docks.storyletdeck' is locked\n * by bob@bob-ws\"); every other file in an all-or-nothing batch reports only that it was not prepared because of\n * that one, which is not worth repeating.\n */\nexport function writeFailure(results: VCWriteOutcome[]): string {\n const failed = results.filter((r) => !r.success);\n const refused = failed.filter((r) => r.status === \"locked\" || r.status === \"outOfDate\");\n const say = (r: VCWriteOutcome): string => {\n const name = basename(r.filePath);\n const why = r.message || r.status;\n return why.includes(name) ? why : `${name} (${why})`;\n };\n if (refused.length > 0) return `nothing was saved: ${refused.map(say).join(\"; \")}`;\n return `couldn't save ${failed.map(say).join(\"; \") || \"the project\"}`;\n}\n\n/** How many steps Undo keeps: ProseMirror's default depth, the same in both apps. A step holds whole files\n * before and after, so an uncapped stack grew for as long as the window stayed open. */\nexport const UNDO_LIMIT = 100;\n\n/**\n * A file an app restores its own way. `same` says whether the file on disk still says what the step expects\n * (the drift check), and `restore` gives the content to write for the target, from what is on disk now. A\n * `.patterx` compares and restores every block but its edit trail, which the undo stamps afresh.\n */\nexport interface FileRestorer {\n same(expected: string | null, current: string | null): boolean;\n restore(target: string | null, current: string | null): string | null;\n}\n\nexport interface HistoryOptions {\n /** How an undo or redo is written. Default `applyStates`. */\n write?: StateWriter;\n /** A restorer for a file the app restores its own way, by path; undefined for every other file. */\n restorer?: (path: string) => FileRestorer | undefined;\n /** How each file's current bytes are read for the drift check. Default from disk (`captureBefore`). */\n read?: (paths: string[]) => FileState[];\n}\n\n/** What an undo or redo came to. `drift` names the files that changed since the step, when that refused it. */\nexport type StepOutcome =\n | { ok: true; label: string }\n | { ok: false; label: string; error: string; drift?: string[] };\n\ninterface Entry {\n label: string;\n key: string;\n before: FileState[];\n after: FileState[];\n}\n\n/** Merge state lists by path, `override` winning. */\nfunction mergeStates(base: FileState[], override: FileState[]): FileState[] {\n const byPath = new Map<string, FileState>();\n for (const s of base) byPath.set(s.path, s);\n for (const s of override) byPath.set(s.path, s);\n return [...byPath.values()];\n}\n\n/** File names, as a refusal lists them. */\nconst listNames = (paths: string[]): string => paths.map((p) => basename(p)).join(\", \");\n\nexport class History {\n private undoStack: Entry[] = [];\n private redoStack: Entry[] = [];\n private readonly opts: HistoryOptions;\n\n constructor(opts: HistoryOptions = {}) {\n this.opts = opts;\n }\n\n reset(): void {\n this.undoStack = [];\n this.redoStack = [];\n }\n\n get canUndo(): boolean { return this.undoStack.length > 0; }\n get canRedo(): boolean { return this.redoStack.length > 0; }\n /** The labels of the next undo and redo, for a menu (\"Undo Rename Card\"). */\n get undoLabel(): string | undefined { return this.undoStack[this.undoStack.length - 1]?.label; }\n get redoLabel(): string | undefined { return this.redoStack[this.redoStack.length - 1]?.label; }\n\n /** Record a committed change; coalesce with the top entry when the key matches (the same logical edit\n * continuing, as typing into one field). A new distinct edit clears redo. */\n record(label: string, key: string, before: FileState[], after: FileState[]): void {\n const top = this.undoStack[this.undoStack.length - 1];\n if (top && top.key === key) {\n top.before = mergeStates(before, top.before); // keep the earliest before per path\n top.after = mergeStates(top.after, after); // keep the latest after per path\n } else {\n this.undoStack.push({ label, key, before, after });\n if (this.undoStack.length > UNDO_LIMIT) this.undoStack.shift();\n }\n this.redoStack = [];\n }\n\n /**\n * Fold a write the app made on its own into the step the author took last, rather than recording a step of\n * its own: for a write nobody asked for (a value stored the first time a view is drawn), which a step of its\n * own would have the author's next Undo undo instead of what they meant. With nothing to undo yet, there is\n * nothing to fold into, and the write simply stands.\n */\n fold(before: FileState[], after: FileState[]): void {\n const top = this.undoStack[this.undoStack.length - 1];\n if (!top) return;\n const known = new Set(top.before.map((s) => s.path));\n top.before = [...top.before, ...before.filter((s) => !known.has(s.path))];\n top.after = mergeStates(top.after, after);\n }\n\n /** Move the top undo entry to redo and return its before-states, for a caller that writes them itself (and\n * checks drift itself). Most callers want `applyUndo`. */\n undo(): FileState[] | undefined {\n const entry = this.undoStack.pop();\n if (!entry) return undefined;\n this.redoStack.push(entry);\n return entry.before;\n }\n\n /** Move the top redo entry to undo and return its after-states (see `undo`). */\n redo(): FileState[] | undefined {\n const entry = this.redoStack.pop();\n if (!entry) return undefined;\n this.undoStack.push(entry);\n return entry.after;\n }\n\n /** Forget every undone step: for a host that changes the files on its own (a step merged into the last one,\n * a reload), after which redoing what was undone would put back the wrong bytes. */\n clearRedo(): void {\n this.redoStack = [];\n }\n\n /** An undo whose states could not be written: the step goes back where it was, so the author can try again\n * once the file can be written. */\n undoFailed(): void {\n const entry = this.redoStack.pop();\n if (entry) this.undoStack.push(entry);\n }\n\n /** The same for a redo that could not be written. */\n redoFailed(): void {\n const entry = this.undoStack.pop();\n if (entry) this.redoStack.push(entry);\n }\n\n /**\n * Undo the top step: check every file still says what the step left it saying, then write its befores. A\n * file that has changed since refuses the undo, and the step is dropped with every step below it (they were\n * taken on top of what has now been overwritten). A write that fails puts the step back. Undefined when there\n * is nothing to undo.\n */\n async applyUndo(): Promise<StepOutcome | undefined> {\n return this.apply(\"undo\");\n }\n\n /** Redo the top undone step, by the same rules: drift drops it and every step above it on the redo stack. */\n async applyRedo(): Promise<StepOutcome | undefined> {\n return this.apply(\"redo\");\n }\n\n private async apply(way: \"undo\" | \"redo\"): Promise<StepOutcome | undefined> {\n const from = way === \"undo\" ? this.undoStack : this.redoStack;\n const entry = from[from.length - 1];\n if (!entry) return undefined;\n const expected = way === \"undo\" ? entry.after : entry.before;\n const target = way === \"undo\" ? entry.before : entry.after;\n const restorer = this.opts.restorer;\n const current = new Map((this.opts.read ?? captureBefore)(target.map((s) => s.path)).map((s) => [s.path, s.content]));\n\n // The drift check, over every file the step touches.\n const drifted = expected.filter((s) => {\n if (s.local === true) return false; // bookkeeping outside version control is the app's own\n const now = current.get(s.path) ?? null;\n const r = restorer?.(s.path);\n return r ? !r.same(s.content, now) : now !== s.content;\n }).map((s) => s.path);\n if (drifted.length > 0) {\n // Every step from here down was taken on top of what has changed: none can be applied safely.\n from.length = 0;\n const name = drifted.length === 1 ? `${basename(drifted[0]!)} has` : `${listNames(drifted)} have`;\n return {\n ok: false, label: entry.label, drift: drifted,\n error: `couldn't ${way} \"${entry.label}\": ${name} changed since, so ${way === \"undo\" ? \"undoing\" : \"redoing\"} it would overwrite that. `\n + `Nothing from that step ${way === \"undo\" ? \"back\" : \"on\"} can be ${way === \"undo\" ? \"undone\" : \"redone\"} now.`,\n };\n }\n\n const states = target.map((s) => {\n const r = restorer?.(s.path);\n return r ? { ...s, content: r.restore(s.content, current.get(s.path) ?? null) } : s;\n });\n from.pop();\n (way === \"undo\" ? this.redoStack : this.undoStack).push(entry);\n const applied = await (this.opts.write ?? applyStates)(states);\n if (!applied.ok) {\n if (way === \"undo\") this.undoFailed(); else this.redoFailed();\n return { ok: false, label: entry.label, error: applied.error };\n }\n return { ok: true, label: entry.label };\n }\n}\n"]}
package/dist/index.cjs CHANGED
@@ -3279,7 +3279,7 @@ function fieldRow(initial) {
3279
3279
  const help = valueHelpText(opts.decl, opts.value);
3280
3280
  valueHelp.textContent = help;
3281
3281
  valueHelp.hidden = help === "";
3282
- const words = [typing || fieldValueProblem(opts.decl, opts.value), opts.problem ?? ""].filter((w) => w !== "");
3282
+ const words = opts.replaceProblem !== void 0 ? [opts.replaceProblem].filter((w) => w !== "") : [typing || fieldValueProblem(opts.decl, opts.value), opts.problem ?? ""].filter((w) => w !== "");
3283
3283
  problem.textContent = words.join(" ");
3284
3284
  problem.hidden = words.length === 0;
3285
3285
  row.classList.toggle("has-problem", words.length > 0);