feat: resolve conflicts through focused two-way drafts

Replace the marker-filled three-pane resolver with a marker-free native
Draft versus frozen Alternative projected from current working text.
Preserve preexisting outside edits, native undo, dirty-save guards, all
sides/bases, and draft caret/viewports across separate History views.
Reuse DiffState alignment/highlighting/excerpts for Resolution and merge
its base-aligned rows for readonly three-way History. Hide explorer and
source-selector chrome by default.

Verified 926 tests plus actual terminal projection, History highlighting,
hunk choice, optional explorer, and guarded jj-confirmed save workflows.
Clippy completes with pre-existing warnings only. Smoke fixtures stay
unresolved in this ancestry; remove that ancestry before publication.

Size exception: approximately 1650 implementation added/deleted lines,
excluding tests/docs, kept coherent because native draft lifecycle and
aligned rendering must change together without parallel legacy modes.
This commit is contained in:
Valdemar Lorenzen
2026-10-14 16:40:23 +02:00
parent f2d505b952
commit ef4c38d8a7
12 changed files with 2292 additions and 421 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ Comparison views reserve no separate metadata header or native-buffer help foote
Each comparison owns its split/unified mode and split axis; startup defaults apply only when opening a comparison. Source alignment and file-side availability stay independent of presentation. Unified display rows carry a side projection: equal context maps both sources, changed runs emit Before rows followed by After rows, and folds retain aligned coordinates. Native navigation/editing continues to bind source buffers rather than a synthetic unified document. The TUI reuses side-specific syntax/wrap/overlay pipelines in one unified region or the existing axis-based split geometry, exposing only the active logical side for unified hit-testing and directional focus.
The resolver owns a dedicated native tab with immutable input buffers and shared editable Result buffers, independent of historical comparison state. `resolve` reads all alternating jj merge-tree terms from immutable Git-backed `jj:trees` metadata; marker parsing only provides region navigation and draft replacement. Session background jobs load inputs and perform guarded saves. The write path holds jj's working-copy lock while validating checkout identity and on-disk data and installing the chosen result, then delegates snapshot/status authority to jj. Save aliases and shared Result tabs route through the same guards. TUI resolver geometry reuses native document rendering, with two inputs above Result or a single selected input at narrow widths.
The resolver owns a dedicated native tab with an editable Draft and frozen read-only Alternatives projected from the current raw working buffer. Projection replaces only conflict regions; automatically merged text and pre-existing outside edits remain identical across initial alternatives. Native undo includes initial seeding. Resolution reuses DiffState alignment, excerpts, highlighting and viewport mapping, including equal-text editing. A separate read-only History uses the original jj inputs/bases; native buffer identity and frontend offsets retain draft/view state. `resolve` reads all alternating Git-backed `jj:trees` terms. Background guarded saves retain checkout/disk validation and jj snapshot/status authority; no presentation change bypasses these write guards.
`Editor::scroll_distance()` supplies the session-wide optional edge margin. The TUI applies it to document/diff visual rows and unwrapped text columns, and retains independent picker offsets across navigation frames. Picker windows exclude their fixed chrome and align whole multi-row entries. File projections carry a monotonic viewport revision, so catalog/filter/expansion changes invalidate picker identity without hashing the entire catalog on every redraw. Metadata-only status changes do not move the viewport. Explicit LSP/wrap-toggle anchors and detached wheel views retain precedence.
+1 -1
View File
@@ -223,7 +223,7 @@ Normal `C` / `D` / `Y` change/delete/copy the cursor-to-line-end suffix through
| Normal | `follow_link` |
| Normal | `diff`, `diff.mode.toggle`, `diff.view.toggle`, `diff.orientation.toggle`, `diff.review.toggle` |
| Normal, Insert, Visual, Command, Search | `diff.close` |
| Normal | `resolve`, `resolve.next`, `resolve.previous`, `resolve.use_source`, `resolve.source`, `resolve.base`, `resolve.next_file`, `resolve.previous_file`, `resolve.files`, `resolve.result`, `resolve.close` |
| Normal | `resolve`, `resolve.next`, `resolve.previous`, `resolve.use_source`, `resolve.source`, `resolve.base`, `resolve.next_file`, `resolve.previous_file`, `resolve.files`, `resolve.draft`, `resolve.history`, `resolve.view.toggle`, `resolve.close` |
| Normal, Insert, Visual, Command, Search | `word_wrap.toggle` |
| Normal, Insert, Visual, Command, Search | `git_blame.toggle` |
| Normal | `pane.split_horizontal`, `pane.split_vertical`, `pane.focus_left`, `pane.focus_down`, `pane.focus_up`, `pane.focus_right`, `pane.close`, `pane.force_close` |
+1 -1
View File
@@ -58,7 +58,7 @@ Diff checks cover staged category/filter navigation, stale request cancellation,
Conflicted-revision regressions preserve unchanged symlinks, defer oversized unselected blobs until file selection, compare selected conflict text with jj's projection, and exclude unchanged conflicts when comparing later revisions. Live terminal smoke covers local bookmark selection and opening an actual conflicted file without changing the checkout.
Resolver checks use isolated real jj repositories: read-only discovery, all sides/bases, per-region choice and undo, unsaved-buffer reuse, retained resolved files, jj-authoritative status, checkout/external-write refusal (including forced save), cancellation, nontext bytes/types/modes/deletion, and directory-child protection. Render checks cover native Result caret/focus, remapped pane movement and full/narrow layouts. Live PTY smoke resolves multiple text regions and binary/symlink whole-file choices, saving and verifying jj status without touching user repositories.
Resolver checks use isolated real jj repositories: read-only discovery, marker-free all-side/base projections preserving outside edits and EOF, initial seed undo, hunk choice/undo, retained resolved files, jj-authoritative status, checkout/external-write refusal, cancellation, nontext bytes/types/modes/deletion, and directory-child protection. Render checks cover aligned gaps/highlights, context folding, native Draft focus/search/selection, separate History, hidden/on-demand explorer, viewport retention and narrow layouts. Live PTY smoke exercises frozen marker-free alternatives, History/Draft switching, hunk choice and guarded save confirmation without touching user repositories.
Repository gutter checks cover LF/CRLF normalization, additions/replacements/deletion anchors, staged/disk/unsaved Git text, unborn and missing named files, ignored/unsupported paths, read-only colocated/non-colocated jj queries, stale results and per-buffer isolation, plus rendered modes, colors, current-line precedence, wrap/scroll and narrow/split geometry. Live PTY smoke verifies all four modes, edit/undo refresh, external Git HEAD changes, and jj parent changes without editor restart.
+11 -8
View File
@@ -458,21 +458,24 @@ Binary data, missing final newlines, permission/type changes, symlink targets, a
## Jujutsu conflict resolution
Run **`:resolve`** in Normal mode for the current jj working copy. It opens a dedicated tab, selects the current file if conflicted (otherwise the first conflict), and focuses its existing Result buffer at the first unresolved region. Already-open unsaved Result text and undo history are retained. Historical comparisons remain read-only; resolving never implicitly checks out or creates another change.
Run **`:resolve`** in Normal mode for the current jj working copy. It opens a dedicated tab and selects the current conflicted file (otherwise the first conflict). Historical comparisons remain read-only; resolving never implicitly checks out or creates another change.
Two read-only inputs appear above the editable Result, with a conflicted-file list on the right. Narrow terminals show one selected input above Result. Input labels identify numbered sides/bases and contributing changes/bookmarks when an exact tree match is available; otherwise they identify the source tree. All merge sides and bases remain selectable, including multi-sided jj conflicts. Inputs support native navigation, search, selection and copying, but reject edits, saves and mutating plugin effects.
**Resolution** shows an editable **Draft** on the left and a read-only **Alternative** on the right, with aligned rows, line/character highlighting, context folds and synchronized scrolling. Both are projected from the current working buffer, including pre-existing unsaved edits: only conflict-marker regions are replaced by their selected alternatives. Already-merged text and edits outside those regions appear on both sides. The initial Draft is seeded from the first side, labelled in its border, and is a normal undoable buffer change; opening never writes it to disk. Alternatives remain frozen while later edits change only the Draft.
| Command | Behavior |
| --- | --- |
| `resolve.next` / `resolve.previous` | Jump to the next/previous unresolved region |
| `resolve.source [N]` | Focus a numbered input; without N, cycle inputs |
| `resolve.base [N]` | Focus a numbered base; default first base |
| `resolve.use_source [N]` | Replace the current region from that input as one undoable edit; default selected input |
| `resolve.next` / `resolve.previous` | Jump between differing decision hunks |
| `resolve.source [N]` | Select/focus an Alternative; in History, select an outer input; without N, cycle |
| `resolve.base [N]` | Open History and select a numbered base; default first base |
| `resolve.use_source [N]` | Take the current differing hunk from that Alternative as one undoable edit |
| `resolve.next_file` / `resolve.previous_file` | Open another retained file |
| `resolve.files` / `resolve.result` | Focus the file list / editable Result |
| `resolve.files` / `resolve.draft` | Toggle/focus the file explorer / return to the editable Draft |
| `resolve.history` / `resolve.view.toggle` | Show read-only input/base/input History / switch views |
| `resolve.close` | Close the resolver without discarding dirty drafts |
Input indexes are one-based and include bases. Commands also appear in the palette and through corresponding `config.core.resolve` action methods; no new global default bindings are installed. Normal-mode Tab/Shift+Tab cycles visible panes and files. In the list, `/` filters, ordinary selection navigation moves the highlight, and Enter opens it. Native Result editing, search, undo/redo and remapped pane navigation work normally. There is no automatic “accept both”.
**History** is separate from editing: it compares the original inputs and their base, with all sides and bases selectable for multi-sided conflicts. Draft text, undo, caret and viewport survive view switches. The file explorer is hidden by default and available on demand. Narrow Resolution panes retain side-by-side comparison where usable; narrow History shows the focused input. Native editing, copying, search and remapped pane navigation work normally; read-only Alternatives and History reject edits and saves.
Input indexes are one-based and include bases. Commands appear in the palette and through corresponding `config.core.resolve` actions; no new global default bindings are installed. Normal-mode Tab/Shift+Tab cycles visible panes and an open file list. In the list, `/` filters and Enter opens a file. The complete seeded Draft can be saved even when intentional differences remain against the Alternative: those differences are not unresolved conflict markers. There is no automatic “accept both”.
**`:w` saves and checks jj**, asynchronously. Before writing, the resolver locks the working copy and validates the frozen checkout and exact on-disk file state. Checkout changes, external writes and unsafe filesystem ancestors refuse the write, including force-save. Successful writes trigger jj's normal snapshot operation, which may capture other on-disk changes. The list distinguishes unresolved, unsaved, checking, resolved and unconfirmed states. jj—not marker scanning or a manual checkbox—confirms resolution. A failed confirmation keeps saved contents and reports unconfirmed status. Resolved files remain in the session for review. Dirty drafts prevent destructive resolver/pane/tab closes.