diff options
| -rw-r--r-- | docs/helix-keys.md | 36 | ||||
| -rw-r--r-- | docs/selections.md | 22 | ||||
| -rw-r--r-- | src/Output.zig | 2 | ||||
| -rw-r--r-- | src/Terminal.zig | 8 | ||||
| -rw-r--r-- | src/Text.zig | 4 | ||||
| -rw-r--r-- | src/edit.zig | 127 | ||||
| -rw-r--r-- | src/look.zig | 2 | ||||
| -rw-r--r-- | src/mouse.zig | 4 | ||||
| -rw-r--r-- | src/normal.zig | 6 | ||||
| -rw-r--r-- | src/tagline.zig | 4 | ||||
| -rw-r--r-- | test/hxcases/cases.jsonl | 15 | ||||
| -rw-r--r-- | test/hxcases/goldens.jsonl | 15 | ||||
| -rw-r--r-- | test/hxcases/waivers.jsonl | 2 | ||||
| -rw-r--r-- | test/mode.zig | 6 | ||||
| -rw-r--r-- | test/panes.zig | 4 | ||||
| -rw-r--r-- | transactions.txt | 15 |
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 |
