summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 16:04:12 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitc811c34ed3bcd23954414125b45c7cac67307d5d (patch)
tree970cd7c021fb808d574180874e852683be778531
parent767aefd1e1cfd79e2a240a59d2d16bfb326f062d (diff)
downloadpardes-c811c34ed3bcd23954414125b45c7cac67307d5d.tar.gz
pardes-c811c34ed3bcd23954414125b45c7cac67307d5d.zip
Every range lives through insert mode, carried through each edit
Insert mode dropped the selection on the first key and restored only the primary's appended-over span on Esc. Now each insert-mode edit is made at the bare cursor as before, and the range is then carried through it with helix's Range::map rules: typing slides an i range and stretches an a range, Enter (which now also drops the blanks before the cursor, as helix does) slides or stretches by what the cursor moved, and an arrow collapses. The replay gives every other range the same. Esc after a pulls each range's end back by one (helix restore_cursor), which replaces Text.append_at. The wiX-edit-drops-sel and msel-append waivers are gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/helix-keys.md36
-rw-r--r--docs/selections.md22
-rw-r--r--src/Output.zig2
-rw-r--r--src/Terminal.zig8
-rw-r--r--src/Text.zig4
-rw-r--r--src/edit.zig127
-rw-r--r--src/look.zig2
-rw-r--r--src/mouse.zig4
-rw-r--r--src/normal.zig6
-rw-r--r--src/tagline.zig4
-rw-r--r--test/hxcases/cases.jsonl15
-rw-r--r--test/hxcases/goldens.jsonl15
-rw-r--r--test/hxcases/waivers.jsonl2
-rw-r--r--test/mode.zig6
-rw-r--r--test/panes.zig4
-rw-r--r--transactions.txt15
16 files changed, 194 insertions, 78 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md
index 90efc463..35f62765 100644
--- a/docs/helix-keys.md
+++ b/docs/helix-keys.md
@@ -7,10 +7,17 @@ checkout (`~/05-genizah/helix`); that checkout now sits on the local
`pardes-harness` branch, whose three commits add only the harness, so
`book/src/keymap.md` is byte-identical at its tip (694e7dfd). As of phase 5
every helix-equivalent row in A and B is differentially verified against real
-helix (see "Differential testing" at the bottom). Five of the six waivers are
-named in the row they belong to; the sixth, `wiX-edit-drops-sel`, belongs to no
-single key — it is the anchor-only divergence every insert-mode edit shares —
-and is described in the Files list at the bottom instead.
+helix (see "Differential testing" at the bottom). Every waiver is named in the
+row it belongs to.
+
+**The reference helix is fixed at one commit:** the `hx-harness` built from
+`694e7dfdd` on the genizah checkout's `pardes-harness` branch, which is upstream
+`278b24389` (2026-06-29, `25.07-905`) plus the three harness commits. "helix"
+in this document means that build, not the 25.07.1 release, which differs on
+counted `Alt-(`/`Alt-)` and on `&`. Upstream master was 82 commits further on
+2026-09-28 (`079a789e`); of those only `416a0e09` (continuing a comment in an
+injected `comment` layer) touches the editing code, and it needs tree-sitter,
+which the harness runs without.
Code map, by SYMBOL — line numbers rot, names do not. Body-normal key
RECOGNITION is `modal.Normal` in `src/modal.zig`: a state machine over `Role` (one
@@ -227,7 +234,7 @@ grew `sels` + `primary`, emitted only when there is more than one range.
| `y` with several ranges | one value per range, pasted back value i at range i by `p`/`P`/`R` and insert `Ctrl-r`; the acme cut/paste chords and a paste into a terminal take the values joined by newlines | `Registers.zig` | helix-verified |
| `"<reg>` | the next command's register: any character names one of its own. Computed ones: `_` swallows writes and reads nothing (`"_d`), `#` is each range's number from 1, `.` each range's text, `%` the file's name, `/` the last `s`/`S` pattern; `+`/`*` the system clipboard. A count typed before `"` stays the command's | helix `select_register` | helix-verified |
| insert `Ctrl-r <reg>` | type this range's value of the register (`Ctrl-r #` numbers the cursors). Esc after `Ctrl-r` only cancels it. `Ctrl-r +` reads what pardes last put on the clipboard, not the desktop's | helix `insert_register` | helix-verified |
-| `a` … `Esc` with several ranges | the primary's appended-over span is restored; the others collapse to bare cursors | `Pane.append_at` is a single field. Waived (`msel-append`) | waived |
+| insert mode with a selection | every range is carried through each edit as helix maps it (`Range::map`): typing at the head of an `i` range slides it, typing at the end of an `a` range stretches it, Enter slides or stretches it by what the cursor moved, and an arrow key collapses it. Esc after `a` pulls each range's end back one character (helix `restore_cursor`) | `edit.insertKey`, `Text.restore_cursor` | helix-verified |
| `&` | align selections into columns: the k-th range of each line is column k, and spaces go in before each range until its head reaches the column's widest head, in display cells (`File.rawDisplayCol`) | helix `align_selections` as of the harness's helix (25.07.1 grouped columns differently). A range over several lines refuses. A TAB counts as `tab_width` cells, not up to the next stop | helix-verified |
| `Alt-)` / `Alt-(` / `<n>Alt-)` | rotate the CONTENTS of the selections forward / back by one range (n ranges), in one edit; each range comes back over the text it now holds and the primary moves with its text | helix `rotate_selection_contents_*` as of the harness's helix: 25.07.1 read the count as a GROUP size instead (rotate by one within each run of n ranges) and left the primary where it was, which is what helix-golf's `invert_dictionary_2` relies on | helix-verified |
@@ -389,11 +396,11 @@ Files (all in `test/hxcases/`):
(runs the `hx-harness` binary from the helix checkout; override with
`$HX_HARNESS`). Only needed when cases change — the diff itself runs
offline.
-- `waivers.jsonl` — named exemptions, each with a reason. Three live ones:
- `wiX-edit-drops-sel` and `msel-append` (helix maps selections through
- insert-mode edits, pardes does not) and `alt-c-window-op` (the pardes
- window op deliberately shadows helix change-noyank). `msel-yank-paste`
- went with registers of one value per range; `sel-regex-caret` and
+- `waivers.jsonl` — named exemptions, each with a reason. One live one:
+ `alt-c-window-op` (the pardes window op deliberately shadows helix
+ change-noyank). `wiX-edit-drops-sel` and `msel-append` went when insert
+ mode began carrying every range through its edits, `msel-yank-paste` with
+ registers of one value per range, `sel-regex-caret` and
`sel-regex-dot-newline` when `s`/`S` began searching line by line.
- `parity.jsonl` — 80 further cases, used only by the parity gate below.
- `parity-waivers.jsonl` — 14 named exemptions for the parity gate, in three
@@ -412,10 +419,11 @@ Files (all in `test/hxcases/`):
tested when an earlier one misses. The goldens are
`hx-harness test/hxcases/golf.jsonl`; `zig build hxgolf` runs them. All ten examples reproduce their published result in hx 25.07.1 under
the site's own conditions (a file of the example's language, auto-pairs
- on). Under the harness pins two do not: `csv_to_sql` needs auto-pairs to
- close its `VALUES (`, and `invert_dictionary_2` needs 25.07's
- `<count>Alt-(` (rotate within groups of count) where the harness's newer
- helix rotates by count.
+ on). Under the harness two do not, and their goldens are the harness's
+ anyway, which pardes follows: `csv_to_sql` needs auto-pairs (off in the
+ harness and absent in pardes) to close its `VALUES (`, and
+ `invert_dictionary_2` needs 25.07.1's `<count>Alt-(` (rotate within groups
+ of count) where the reference helix rotates by count.
- `smoke.jsonl` — 20 cases referenced by nothing in the tree: no build step,
no script. Either wire it up or delete it.
diff --git a/docs/selections.md b/docs/selections.md
index 0f000e2c..c08ccb14 100644
--- a/docs/selections.md
+++ b/docs/selections.md
@@ -3,6 +3,8 @@
How normal mode holds and changes its selections, and where that differs from
helix's `Selection` (a list of ranges over gap offsets, with a primary index).
Key-by-key behaviour is in docs/helix-keys.md; this is the model underneath.
+"helix" here is the reference build docs/helix-keys.md names: `hx-harness`
+at `694e7dfdd`, upstream `278b24389`.
## What a selection is
@@ -25,8 +27,9 @@ plus up to 63 others.
`s`, `n`/`N`, ...) rather than left behind by a motion. Only pardes reads
it: the look (Enter) and execute (Tab) chords act on explicit selections
and on the word under the cursor otherwise.
-- `select` is `v` extend mode. `append_at` remembers where an `a` began, so
- Esc can give back the range it appended over.
+- `select` is `v` extend mode. `restore_cursor` says the insert session
+ began with `a`, so Esc gives each range back the character it was
+ stretched by.
`Text.setRanges` is the one writer of a whole selection: it sorts by start,
merges ranges that overlap or share a start (helix `normalize`), follows the
@@ -67,6 +70,17 @@ A parsed action has a scope (`modal.Normal.Action.scope`).
look) runs once from the primary and drops back to one cursor
(`normal.multiOnce`).
+## Insert mode
+
+Each range lives on through insert mode. Every insert-mode edit happens at
+the cursor, so `edit.insertKey` runs it on the bare cursor and then carries
+the range through what it did to the text, with helix's `Range::map` rules:
+at an insertion, a backward range's head and an empty range move past it
+and a forward range's head stays before it, so typing slides an `i` range
+and stretches an `a` range. Enter slides or stretches the range by what the
+cursor moved; an arrow key collapses it. The other ranges get the same by
+the replay.
+
## Undo, registers, repeat
- One keystroke is one undo step however many ranges it edited: only the
@@ -86,10 +100,6 @@ A parsed action has a scope (`modal.Normal.Action.scope`).
## Where it differs from helix
-- Insert mode: helix maps every range through each keystroke's edit, so a
- range an `i`/`a` started from stretches over what is typed and keeps its
- anchor. Here only the cursor cells move, and after Esc only the primary's
- `append_at` span comes back (waivers `wiX-edit-drops-sel`, `msel-append`).
- Direction of a one-character range: both of its cells are the same cell,
so it has none. helix can flip one (`Alt-;`) and the commands that read
the head (`&`, the next extend) see the difference.
diff --git a/src/Output.zig b/src/Output.zig
index 6e1765a7..201b5491 100644
--- a/src/Output.zig
+++ b/src/Output.zig
@@ -749,7 +749,7 @@ pub fn resetBody(p: *Pardes, pane: *Pane) void {
pane.body.select = false;
pane.sel = @splat(.{});
pane.body.sticky_col = -1;
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.normal.clear();
pane.look_at = null;
if (p.look_hover_wait) |wait| if (wait.serial == pane.serial) {
diff --git a/src/Terminal.zig b/src/Terminal.zig
index e204b44c..e3378007 100644
--- a/src/Terminal.zig
+++ b/src/Terminal.zig
@@ -717,7 +717,7 @@ pub fn feedOutput(p: *Pardes, pane: *Pane, bytes: []const u8) void {
if (comptime !enabled) return;
if (pane.terminal == null) return;
const has_positions = pane.ovl != null or pane.body.cur_pinned or pane.body.vsel.active or
- pane.body.msel.active or pane.body.nsel > 0 or pane.body.append_at != null or pane.look_at != null or
+ pane.body.msel.active or pane.body.nsel > 0 or pane.look_at != null or
pane.body.ed_undo_len > 0 or pane.body.ed_redo_len > 0 or pointerRowCount(pane) > 0;
// Sticky bottom, as every other terminal does it: output drags the
// viewport down only when the viewport was already on the last row, so
@@ -843,10 +843,6 @@ fn ingestWithPositions(pane: *Pane, bytes: []const u8) void {
extra[nextra + 1] = &selection.arow;
nextra += 2;
}
- if (pane.body.append_at) |*at| {
- extra[nextra] = &at.row;
- nextra += 1;
- }
if (pane.look_at) |*at| {
extra[nextra] = &at.row;
nextra += 1;
@@ -1124,7 +1120,7 @@ pub fn enterTty(p: *Pardes, id: usize) void {
// keeps its modal endpoint for the next return to editor mode.
pane.body.cur_pinned = selected;
pane.body.select = false;
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
pane.body.normal.clear();
}
diff --git a/src/Text.zig b/src/Text.zig
index 7ad243a1..764b8a6a 100644
--- a/src/Text.zig
+++ b/src/Text.zig
@@ -65,7 +65,9 @@ select: bool = false,
/// sticky goal column for j/k runs (helix old_visual_position): any
/// non-vertical range write resets it to -1.
sticky_col: i32 = -1,
-append_at: ?struct { row: i32, col: i32 } = null,
+/// The insert session began with `a`: on Esc every range gives back the
+/// one character it was stretched by (helix restore_cursor).
+restore_cursor: bool = false,
/// insert `Ctrl-r` was pressed: the next key names the register to insert
reg_wait: bool = false,
normal: modal.Normal.State = .{},
diff --git a/src/edit.zig b/src/edit.zig
index 14d8b7ed..8f6f09c6 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -344,28 +344,20 @@ pub fn exitInsert(p: *Pardes, t: *Text) void {
t.reg_wait = false;
t.msel.active = false;
t.normal.clear();
- if (t.append_at) |aa| {
- t.append_at = null;
- const text = flatSurface(p, t) catch return;
- const gap = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
- const a_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, aa.row)), .col = @intCast(@max(0, aa.col)) });
- const back = @max(a_off, modal.prevGrapheme(text, gap));
- const bc = modal.positionAt(text, back);
- t.cur_row = @intCast(bc.row);
- t.cur_col = @intCast(bc.col);
- t.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false };
- t.cur_pinned = true;
- t.show();
- for (t.sels[0..t.nsel]) |*s| {
- const sgap = modal.offsetAt(text, .{ .row = @intCast(@max(0, s.row)), .col = @intCast(@max(0, s.col)) });
- const b2 = if (back == gap) sgap else modal.prevGrapheme(text, sgap);
- const bp = modal.positionAt(text, b2);
- s.row = @intCast(bp.row);
- s.col = @intCast(bp.col);
- s.arow = s.row;
- s.acol = s.col;
- }
+ if (!t.restore_cursor) return;
+ t.restore_cursor = false;
+ // helix enter_normal_mode: every range, from its start to the char
+ // before its end when it points forward, else to its end
+ const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active;
+ const text = flatSurface(p, t) catch return;
+ var rs: [Text.max_selections]modal.Selection = undefined;
+ const got = t.ranges(text, 0, &rs);
+ for (rs[0..got.n]) |*r| {
+ const from = @min(r.anchor, r.head);
+ const to = @max(r.anchor, r.head);
+ r.* = .{ .anchor = from, .head = if (r.head > r.anchor) @max(from, modal.prevGrapheme(text, to)) else to };
}
+ t.setRanges(text, rs[0..got.n], &.{}, got.pri, expl);
}
pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 {
@@ -518,7 +510,7 @@ pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void {
// `u` undoes the whole session and restores what was selected
pushUndo(p, t);
t.select = false;
- t.append_at = null;
+ t.restore_cursor = false;
t.sticky_col = -1;
const text = flatSurface(p, t) catch {
t.mode = .insert;
@@ -544,12 +536,14 @@ pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void {
const lo_col = if (b) |bb| bb.lo_col else t.cur_col;
const hi_row = if (b) |bb| bb.hi_row else t.cur_row;
const hi_col = if (b) |bb| bb.hi_col else t.cur_col;
- t.append_at = .{ .row = lo_row, .col = lo_col };
+ t.restore_cursor = true;
const gap = modal.nextGrapheme(text, modal.offsetAt(text, .{ .row = @intCast(@max(0, hi_row)), .col = @intCast(@max(0, hi_col)) }));
const gc = modal.positionAt(text, gap);
t.cur_row = @intCast(gc.row);
t.cur_col = @intCast(gc.col);
- t.vsel = .{ .active = b != null, .row = lo_row, .col = lo_col, .explicit = false };
+ // helix append_mode: the range reaches one past its end, which
+ // is where typing goes; a bare cursor becomes a range too
+ t.vsel = .{ .active = true, .row = lo_row, .col = lo_col, .explicit = false };
t.cur_pinned = true;
},
.line_start => {
@@ -634,6 +628,72 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
t.reg_wait = true;
return;
}
+ // helix maps every range through each insert-mode edit (Range::map):
+ // the edits below work on the bare cursor, and the range the cursor
+ // heads is carried through what they did to the text afterwards
+ if (!t.vsel.active) return insertEdit(p, t, key);
+ const old = flatSurface(p, t) catch return insertEdit(p, t, key);
+ const r = t.primaryRange(old, 0);
+ const c = modal.selectionCursor(old, r);
+ const old_len = old.len;
+ const explicit = t.vsel.explicit;
+ t.vsel.active = false;
+ insertEdit(p, t, key);
+ if (t.mode != .insert) return;
+ const new = flatSurface(p, t) catch return;
+ const c2 = modal.offsetAt(new, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
+ // every insert-mode edit happens at the cursor: an insertion there, or
+ // a deletion that ends there (backward) or starts there (forward)
+ if (key.cp == Key.enter) {
+ // helix insert_newline: an appending range stretches by what the
+ // cursor moved, any other slides by it
+ const d = @as(isize, @intCast(c2)) - @as(isize, @intCast(c));
+ const head: usize = @intCast(@as(isize, @intCast(r.head)) + d);
+ const anchor: usize = if (c > r.anchor) r.anchor else @intCast(@as(isize, @intCast(r.anchor)) + d);
+ return setInsertRange(t, new, .{ .anchor = anchor, .head = head }, explicit);
+ }
+ var at: usize = c;
+ var del: usize = 0;
+ var ins: usize = 0;
+ if (new.len > old_len and c2 == c + (new.len - old_len)) {
+ ins = new.len - old_len;
+ } else if (new.len < old_len and c2 + (old_len - new.len) == c) {
+ at = c2;
+ del = old_len - new.len;
+ } else if (new.len < old_len and c2 == c) {
+ del = old_len - new.len;
+ } else if (new.len != old_len or c2 != c) {
+ return; // a motion: the range collapses onto the cursor (helix)
+ }
+ // helix Assoc: the head sticks after an insertion at it when the range
+ // points backward or is empty, the anchor when it points forward
+ const fwd = r.anchor < r.head;
+ setInsertRange(t, new, .{
+ .anchor = mapThroughEdit(r.anchor, at, del, ins, !fwd and r.anchor != r.head),
+ .head = mapThroughEdit(r.head, at, del, ins, fwd),
+ }, explicit);
+}
+
+fn setInsertRange(t: *Text, new: []const u8, mapped: modal.Selection, explicit: bool) void {
+ const cells = Text.rangeCells(new, mapped);
+ const cc = modal.positionAt(new, cells.cur);
+ const ac = modal.positionAt(new, cells.anc);
+ t.cur_row = @intCast(cc.row);
+ t.cur_col = @intCast(cc.col);
+ t.vsel = .{ .active = cells.anc != cells.cur, .row = @intCast(ac.row), .col = @intCast(ac.col), .explicit = explicit };
+}
+
+/// A position through one edit at `at` that deleted `del` bytes and put
+/// `ins` in (one of the two is 0). A position exactly at an insertion stays
+/// before it when `before` is set, else moves past it.
+fn mapThroughEdit(pos: usize, at: usize, del: usize, ins: usize, before: bool) usize {
+ if (pos < at) return pos;
+ if (del > 0) return if (pos < at + del) at else pos - del;
+ if (pos == at and before) return pos;
+ return pos + ins;
+}
+
+fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
t.vsel.active = false;
if (!t.cur_pinned) pinPaneCursor(p, t);
_ = clampFileCursor(t);
@@ -748,12 +808,29 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
switch (key.cp) {
Key.enter => {
const line = modal.lineSlice(text, c.row);
+ const before = line[0..@min(c.col, line.len)];
+ // helix insert_newline: after nothing but blanks, the line
+ // itself moves down, blanks and all; else the blanks before the
+ // cursor go and the new line takes the indent
+ if (std.mem.indexOfNone(u8, before, " \t") == null) {
+ const new = modal.insertAt(p.gpa, text, .{ .row = c.row, .col = 0 }, "\n") catch return;
+ setEditText(p, t, new);
+ t.cur_row += 1;
+ t.cur_pinned = true;
+ t.show();
+ return;
+ }
const indent = modal.newlineIndentWidth(line, c.col);
const arena = p.scratch.allocator();
const block = arena.alloc(u8, 1 + indent) catch return;
block[0] = '\n';
@memset(block[1..], ' ');
- const new = modal.insertAt(p.gpa, text, c, block) catch return;
+ // a tag keeps its blanks: they part its words, and the last
+ // may be its read-only prefix's
+ const kept = if (t.what == .body) std.mem.trimEnd(u8, before, " \t").len else c.col;
+ const cut = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = kept }, c) catch return;
+ defer p.gpa.free(cut);
+ const new = modal.insertAt(p.gpa, cut, .{ .row = c.row, .col = kept }, block) catch return;
setEditText(p, t, new);
t.cur_row += 1;
t.cur_col = @intCast(indent);
diff --git a/src/look.zig b/src/look.zig
index 5e2d6068..30fb0dab 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -1016,7 +1016,7 @@ pub fn clearNavigationSelection(p: *Pardes, pane: *Pane) void {
pane.body.nsel = 0;
pane.body.select = false;
pane.body.normal.clear();
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
for (0..pane.sel.len) |slot| pane.clearPointerSelection(slot);
pane.sel = @splat(.{});
diff --git a/src/mouse.zig b/src/mouse.zig
index 91ec41a6..d1c32b40 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -747,7 +747,7 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void {
pane.body.vsel.active = false;
pane.body.nsel = 0;
pane.body.select = false;
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
pane.body.normal.clear();
edit.setYank(p, text);
@@ -859,7 +859,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
pane.body.vsel.active = false;
pane.body.nsel = 0; // a click says WHERE the one cursor is
pane.body.select = false;
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
if (s.ctrl) p.lspRequest(s.id, .definition, "");
}
diff --git a/src/normal.zig b/src/normal.zig
index f5e868a4..53e416fd 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -149,7 +149,7 @@ const KeyState = struct {
normal: modal.Normal.State,
find_op: u8,
find_ch: u21,
- append_at: @FieldType(Text, "append_at"),
+ restore_cursor: bool,
reg_wait: bool,
fn of(t: *Text) KeyState {
@@ -159,7 +159,7 @@ const KeyState = struct {
.normal = t.normal,
.find_op = t.find_op,
.find_ch = t.find_ch,
- .append_at = t.append_at,
+ .restore_cursor = t.restore_cursor,
.reg_wait = t.reg_wait,
};
}
@@ -170,7 +170,7 @@ const KeyState = struct {
t.normal = s.normal;
t.find_op = s.find_op;
t.find_ch = s.find_ch;
- t.append_at = s.append_at;
+ t.restore_cursor = s.restore_cursor;
t.reg_wait = s.reg_wait;
}
};
diff --git a/src/tagline.zig b/src/tagline.zig
index 14bf5677..1c060a66 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -219,7 +219,7 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void {
t.msel.active = false;
t.nsel = 0;
t.select = false;
- t.append_at = null;
+ t.restore_cursor = false;
t.sticky_col = -1;
t.mode = .normal;
t.normal.clear();
@@ -325,7 +325,7 @@ pub fn enterHeader(p: *Pardes, column: ?usize) void {
t.msel.active = false;
t.nsel = 0;
t.select = false;
- t.append_at = null;
+ t.restore_cursor = false;
t.mode = .normal;
t.normal.clear();
}
diff --git a/test/hxcases/cases.jsonl b/test/hxcases/cases.jsonl
index 7ba44f09..ba75c611 100644
--- a/test/hxcases/cases.jsonl
+++ b/test/hxcases/cases.jsonl
@@ -344,6 +344,21 @@
{"name": "i-keeps-sel-start", "pane": "file", "text": "alpha beta\n", "keys": "wi"}
{"name": "wa-sel-end", "pane": "file", "text": "alpha beta\n", "keys": "wa"}
{"name": "wiX-edit-drops-sel", "pane": "file", "text": "alpha beta\n", "keys": "wiX<esc>"}
+{"name": "reg-selection-insert", "pane": "file", "text": "ab\n", "keys": "vli<C-r>.<esc>"}
+{"name": "ins-sel-backspace", "pane": "file", "text": "alpha beta\n", "keys": "wi<backspace><esc>"}
+{"name": "ins-append-enter", "pane": "file", "text": "ab cd\n", "keys": "wa<ret>x<esc>"}
+{"name": "ins-append-backspace", "pane": "file", "text": "ab cd\n", "keys": "ea<backspace><backspace><esc>"}
+{"name": "ins-sel-delete", "pane": "file", "text": "ab cd\n", "keys": "wi<del><esc>"}
+{"name": "ins-sel-arrow-collapses", "pane": "file", "text": "ab cd\n", "keys": "wi<right><esc>"}
+{"name": "msel-insert-keeps-ranges", "pane": "file", "text": "ab cd\nab cd\n", "keys": "wCiX<esc>"}
+{"name": "msel-append-lines", "pane": "file", "text": "aa\nbb\n", "keys": "%<A-s>a!<esc>"}
+{"name": "ins-append-bare", "pane": "file", "text": "ab\n", "keys": "axy<esc>"}
+{"name": "ins-change-then-type", "pane": "file", "text": "ab cd\n", "keys": "wcX<esc>"}
+{"name": "ins-open-below-sel", "pane": "file", "text": "ab\ncd\n", "keys": "xoX<esc>"}
+{"name": "ins-enter-trims-blanks", "pane": "file", "text": "ab cd\n", "keys": "5li<ret><esc>"}
+{"name": "ins-enter-in-indent", "pane": "file", "text": " ab\n", "keys": "2li<ret><esc>"}
+{"name": "ins-enter-after-open", "pane": "file", "text": " ab\n", "keys": "o<ret>x<esc>"}
+{"name": "msel-ins-enter", "pane": "file", "text": "ab cd\nab cd\n", "keys": "wCa<ret><esc>"}
{"name": "a-esc-restore", "pane": "file", "text": "abc\n", "keys": "a<esc>"}
{"name": "ax-restore", "pane": "file", "text": "abc\n", "keys": "ax<esc>"}
{"name": "lax-restore", "pane": "file", "text": "abc\n", "keys": "lax<esc>"}
diff --git a/test/hxcases/goldens.jsonl b/test/hxcases/goldens.jsonl
index f6a6aada..188b7ce4 100644
--- a/test/hxcases/goldens.jsonl
+++ b/test/hxcases/goldens.jsonl
@@ -344,6 +344,21 @@
{"name":"i-keeps-sel-start","text":"alpha beta\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":5}}
{"name":"wa-sel-end","text":"alpha beta\n","mode":"insert","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":0}}
{"name":"wiX-edit-drops-sel","text":"Xalpha beta\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":6}}
+{"name":"reg-selection-insert","text":"abab\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":3}}
+{"name":"ins-sel-backspace","text":"alpha beta\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":5}}
+{"name":"ins-append-enter","text":"ab\nxcd\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":0,"col":0}}
+{"name":"ins-append-backspace","text":" cd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}
+{"name":"ins-sel-delete","text":"b cd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":1}}
+{"name":"ins-sel-arrow-collapses","text":"ab cd\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}}
+{"name":"msel-insert-keeps-ranges","text":"Xab cd\nXab cd\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":3},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":3}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":3}}],"primary":1}
+{"name":"msel-append-lines","text":"aa!\nbb!\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":0}}],"primary":0}
+{"name":"ins-append-bare","text":"axyb\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}}
+{"name":"ins-change-then-type","text":"Xcd\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}}
+{"name":"ins-open-below-sel","text":"ab\nX\ncd\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}
+{"name":"ins-enter-trims-blanks","text":"ab\ncd\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}}
+{"name":"ins-enter-in-indent","text":"\n ab\n","mode":"normal","cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2}}
+{"name":"ins-enter-after-open","text":" ab\n\n x\n","mode":"normal","cursor":{"row":2,"col":5},"anchor":{"row":2,"col":5}}
+{"name":"msel-ins-enter","text":"ab\ncd\nab\ncd\n","mode":"normal","cursor":{"row":2,"col":2},"anchor":{"row":2,"col":0},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}},{"cursor":{"row":2,"col":2},"anchor":{"row":2,"col":0}}],"primary":1}
{"name":"a-esc-restore","text":"abc\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}
{"name":"ax-restore","text":"axbc\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}}
{"name":"lax-restore","text":"abxc\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":1}}
diff --git a/test/hxcases/waivers.jsonl b/test/hxcases/waivers.jsonl
index 8ac3bd9c..2d74e346 100644
--- a/test/hxcases/waivers.jsonl
+++ b/test/hxcases/waivers.jsonl
@@ -1,3 +1 @@
-{"name":"wiX-edit-drops-sel","reason":"Insert edits clear the selection instead of tracking its anchor.","reference":{"name":"wiX-edit-drops-sel","text":"Xalpha beta\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":6}},"expected":{"name":"wiX-edit-drops-sel","text":"Xalpha beta\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}}}
{"name":"alt-c-window-op","reason":"Alt-c moves a pane to a new column instead of Helix's change-noyank.","reference":{"name":"alt-c-window-op","text":"bc def\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},"expected":{"name":"alt-c-window-op","text":"abc def\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}}
-{"name":"msel-append","reason":"Append-mode selection origins are tracked only for the primary selection.","reference":{"name":"msel-append","text":"aza\nbzb\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0}}],"primary":1},"expected":{"name":"msel-append","text":"aza\nbzb\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0}}],"primary":1}}
diff --git a/test/mode.zig b/test/mode.zig
index c7d7d548..cba93c30 100644
--- a/test/mode.zig
+++ b/test/mode.zig
@@ -79,14 +79,14 @@ test "Mode from actual tag Exec preserves parked file and output insert transact
p.update(.{ .key = .{ .cp = pardes.Key.tab } });
try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode);
try std.testing.expect(pane.focus == .body);
- try std.testing.expect(pane.body.append_at == null);
+ try std.testing.expect(!pane.body.restore_cursor);
p.update(.{ .key = .{ .cp = 'u' } });
try std.testing.expectEqualStrings("ab\n", pane.file.?.content);
p.update(.{ .key = .{ .cp = 'a' } });
p.update(.{ .key = .{ .cp = 'z', .text = "z" } });
- try std.testing.expect(pane.body.append_at != null);
+ try std.testing.expect(pane.body.restore_cursor);
try mode(p, 0, .normal);
- try std.testing.expect(pane.body.append_at == null);
+ try std.testing.expect(!pane.body.restore_cursor);
p.update(.{ .key = .{ .cp = 'u' } });
try std.testing.expectEqualStrings("ab\n", pane.file.?.content);
}
diff --git a/test/panes.zig b/test/panes.zig
index cbf9d097..4e98f449 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -4547,7 +4547,7 @@ const LookResetTests = struct {
pane.body.nsel = 1;
pane.body.select = true;
pane.body.normal.count = 5;
- pane.body.append_at = .{ .row = 0, .col = 3 };
+ pane.body.restore_cursor = true;
pane.body.sticky_col = 7;
for (0..pane.sel.len) |slot| {
pane.clearPointerSelection(slot);
@@ -4567,7 +4567,7 @@ const LookResetTests = struct {
try std.testing.expectEqual(@as(u8, 0), pane.body.nsel);
try std.testing.expect(!pane.body.select);
try std.testing.expectEqual(modal.Normal.State{}, pane.body.normal);
- try std.testing.expect(pane.body.append_at == null);
+ try std.testing.expect(!pane.body.restore_cursor);
try std.testing.expectEqual(@as(i32, -1), pane.body.sticky_col);
for (pane.sel) |selection| try std.testing.expectEqual(.none, selection.state);
for (pane.pointer_selections) |selection| try std.testing.expect(selection == null);
diff --git a/transactions.txt b/transactions.txt
index 6c2e7646..8d2fda3f 100644
--- a/transactions.txt
+++ b/transactions.txt
@@ -1,13 +1,8 @@
-Selection anchors through insert-mode edits, and the transaction model
-=======================================================================
-
-STATUS: still open. `wiX-edit-drops-sel` is line 1 of test/hxcases/waivers.jsonl,
-so the gap below describes the code as it stands rather than something already
-fixed. One correction to the sentence that follows: it is not quite "the one
-place". Line 3 of the same file, `msel-append`, says in its own words "Same
-family as wiX-edit-drops-sel: pardes does not map selections through insert-mode
-edits" — so the gap has two waivers, and what is singular is the CAUSE below,
-not the number of divergences it produces.
+STATUS: closed for the waivers. Insert mode now carries every range through
+each edit (edit.insertKey maps the range with helix's Range::map rules after
+the edit is made), so `wiX-edit-drops-sel` and `msel-append` are gone. What
+follows is the original analysis; the delta layer it proposes was not needed
+for that and stays unbuilt.
The one place the helix differential suite shows a real structural gap
(waiver "wiX-edit-drops-sel" in test/hxcases/waivers.jsonl): helix keeps a