docs/internal/live-diff-scalable-diff-design.md
Status: implemented — with one deviation from the original proposal:
per review feedback, no new dependency was added. computeLineDiff is
backed by an in-house patience diff (fresh-plugin-runtime/src/diff.rs:
line interning → unique-line anchors chained by LIS → recursion between
anchors, with a small dense-LCS fallback for anchor-free chunks and a
coarse whole-chunk replacement past FALLBACK_LCS_CELLS) instead of the
imara-diff crate discussed below. The API shape, degraded render
levels, and test strategy are as proposed. The API also ended up
synchronous rather than promise-based: it executes on the plugin
runtime's own thread — the same thread that previously ran the JS DP —
so a native sub-millisecond call needs no async plumbing, and the editor
loop was never involved.
Motivated by a reproduced refusal: checking out HEAD~1200's main.rs
→ 24.9M DP cells → lineDiff bails → plugin renders nothing.
live_diff.ts implements the line diff as a dense LCS DP table:
O(m·n) time and memory over the post-prefix/suffix-strip middle,
with MAX_DP_CELLS = 16M as the safety valve. Two structural problems:
live_diff.ts:75-79 promises a degraded gutter-only path; it was
never implemented.)A robust fix removes both: an algorithm whose worst case is acceptable, and a pipeline whose degradation is local (less detail) instead of global (nothing).
Add a host function and delete the plugin's dense-DP lineDiff
entirely:
// fresh.d.ts
interface LineDiffHunk {
oldStart: number; oldCount: number; // 0-indexed, old side
newStart: number; newCount: number; // 0-indexed, new side
}
/** Line-level diff of two texts. Histogram algorithm, native. */
computeLineDiff(oldText: string, newText: string): Promise<LineDiffHunk[]>;
imara-diff crate (gitoxide's engine:
interned tokens, Histogram algorithm with Myers fallback for
high-occurrence regions). Alternative: similar (Patience/Myers,
broader API incl. word/char modes). Either turns the repro case
into sub-millisecond work; imara-diff is the faster and smaller
dependency, and Histogram gives the best hunk quality on code.#[plugin_api(async_promise, ...)]
machinery in crates/fresh-plugin-runtime/src/ts_export.rs — the
same pattern as spawnProcess/delay. Run the diff off the editor
loop (worker thread, as spawnProcess does) so even worst-case
inputs never block a frame.opsToHunks + fillOldLines
produce today (oldStart/oldCount replaces the _oldStart/_oldEnd
stash), so live_diff.ts integration is a small mechanical change:
refineHunks, rendering, diff_nav view-state publishing all stay
as they are.Array costs, and Myers' worst case (~10⁸ int ops for
a 5k×6k full rewrite) is tens of milliseconds native but seconds
in QuickJS. Native also makes the engine reusable: git_log,
review-diff tooling, and future plugins get one battle-tested diff
instead of per-plugin reimplementations.Sizing: getBufferText already copies the buffer to JS per recompute,
so passing both texts over the bridge adds nothing new. (A later
optimization — diffBufferAgainstText(bufferId, refText) reading the
rope directly host-side — halves the copies, but is not needed for
correctness.)
With Histogram the caps almost never trip, but the pipeline should still be a total function:
MAX_DP_CELLS (dead concept with the host diff).MAX_DIFF_LINES (say the current 100k) as a
render-sanity guard, but change its behavior: instead of clearing
everything, do the O(N) prefix/suffix scan and render one coarse
"replaced block" hunk (gutter ~ on the changed span + one
scrollbar streak), with status
"Live Diff: change too large — showing outline only". That is the
degraded path the code comment already promises.refineHunks runs a per-pair char
LCS (up to 2000×2000 = 4M boxed-array cells per pair). Fine for
a handful of pairs; slow for a 1200-commit drift with hundreds.
Add a global per-recompute budget (e.g. 10M cells, spent
first-hunk-first or viewport-first); pairs past the budget keep
plain modified rendering without word underlines. (If similar
is chosen in step 1, its char-diff mode can replace this JS DP
outright.)status.too_large says the file is too
large; after this change the only remaining message concerns the
change being summarized, and should say so (all 14 locales in
live_diff.i18n.json).computeLineDiff hunks match
git diff --no-index -U0 on the same pair (precedent:
review_diff_hunk_parity.rs).computeLineDiff on the synthetic worst pair
completes within a bound (native Histogram: single-digit ms).git diff --no-index per recompute: needs two
temp-file writes per 75 ms debounce tick, couples the hot path to
process spawn latency (and trust-mode/PATH concerns), and produces
output that must be reparsed. Reference loading already uses git;
the per-keystroke hot path should not.computeLineDiff host API + imara-diff dependency + parity/perf
tests.live_diff.ts: swap lineDiff for the API, delete DP code and
MAX_DP_CELLS, reword statuses.Steps 1–2 alone make the reported scenario a non-issue; step 3 makes the plugin total under any input.