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
+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.