UI half of the 3-way merge feature. Hand-rolls a line-level diff
(LCS + backtrack, no npm dep so the bundle-size guard stays
comfortable) and an Obsidian Modal that shows ancestor / mine /
theirs side-by-side with adds + deletes highlighted.
Pieces:
- `plugin/src/conflict/DiffEngine.ts`
- `diffLines(a, b): DiffChunk[]` — runs LCS on line arrays then
backtracks to a list of {kind, lines} chunks, merging adjacent
same-kind ops. `Int32Array` for the DP matrix to keep big
inputs cheap (a 500-line × 500-line diff stays well under
100 ms — verified via the perf-sanity test).
- Boundary cases handled: empty inputs return zero or one chunk
of the appropriate kind; identical inputs return one `eq` chunk.
- `plugin/src/ui/ThreeWayMergeModal.ts`
- Modal subclass with the prompt() Promise pattern this codebase
uses elsewhere (PendingPluginsModal / WriteConflictModal).
- 3-pane layout via grid: ancestor (plain), mine (diff vs ancestor),
theirs (diff vs ancestor). Adds tinted green, deletes tinted red
+ strikethrough.
- Collapsible 'Edit merged' textarea pre-filled with mine; user can
hand-merge and click 'Save merged'.
- Buttons: Cancel / Keep theirs / Save merged / Keep mine (default).
Escape + outside-click default to cancel — never silently picks
a side.
- `plugin/tests/DiffEngine.test.ts` — 12 cases: empty boundaries,
identity, pure add / pure del, line replacement, chunk-grouping
invariant (no two adjacent chunks share a kind), reconstruction
invariant (diff is lossless), 500-line perf sanity.
Modal isn't unit-tested — Obsidian's `Modal` is .d.ts only, so
behaviour gets verified via manual smoke. The next PR (E2-α.3)
wires the adapter conflict path so the modal can be exercised
end-to-end against the docker test sshd.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>