diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-27 11:45:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | d91c2cc53f816634a5471672bd33b842f06e849e (patch) | |
| tree | 1193b6752db6a62d85d7ef6c06c38d95ae495d43 | |
| parent | 6877a61ef40ad0d8e762a28d0a3f5fa9bfefaff2 (diff) | |
| download | pardes-d91c2cc53f816634a5471672bd33b842f06e849e.tar.gz pardes-d91c2cc53f816634a5471672bd33b842f06e849e.zip | |
tag columns are the WHOLE rendered tag: h/j/k/l walk taglines, the path selects but never edits
| -rw-r--r-- | docs/helix-keys.md | 5 | ||||
| -rw-r--r-- | src/pardes.zig | 190 | ||||
| -rw-r--r-- | src/tutor.txt | 26 | ||||
| -rw-r--r-- | test/snapshots/images.snap | 9 | ||||
| -rw-r--r-- | test/snapshots/tagnav.golden | 372 | ||||
| -rw-r--r-- | test/snapshots/tagnav.snap | 75 | ||||
| -rw-r--r-- | test/snapshots/tagpath.golden | 310 | ||||
| -rw-r--r-- | test/snapshots/tagpath.snap | 69 |
8 files changed, 987 insertions, 69 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md index a479fefc..6a77a28d 100644 --- a/docs/helix-keys.md +++ b/docs/helix-keys.md @@ -87,7 +87,8 @@ with a reason in `test/hxcases/waivers.jsonl`. | `Esc` (insert) | back to normal mode, cursor right after the insertion (no vim left-step) | | helix-verified | | `Enter` (normal) | acme **look** chord: EXPLICIT selection, else file-ish word under cursor | pardes-specific, keep (helix normal-mode Enter unbound). Covers helix `gf`. Implicit motion residue falls back to the cursor word | pardes-specific | | `Tab` (normal) | acme **execute** chord | pardes-specific, keep; explicit-selection rule as Enter | pardes-specific | -| `Ctrl-w` + `h/j/k/l`/arrows | directional pane focus prefix — normal/tty modes only | pardes' own window handling (helix window mode skipped, section C). Runs the SAME `Left`/`Down`/`Up`/`Right` builtins `SPC w h/j/k/l` runs; kept alongside the leader because a pane in raw **tty** mode never sees `SPC` (the shell owns it), so this is the only keyboard way out of one. Insert mode owns `Ctrl-w` = delete-word-back; `Ctrl-w` while editing a tag (tags are always insert) is a no-op | pardes-specific | +| `:` (normal, body) | focuses the pane's OWN tag as a one-line editor in **normal** mode, parked at the first EDITABLE column: motions (`w` `b` `e` `W` `B` `E`, `0` `$` `^`, arrows, `Home`/`End`) walk the whole rendered tag, `y` yanks the selection, `Enter`/`Tab` look/execute it (else the file-ish word under the cursor), `i`/`a`/`I`/`A` enter insert, `Esc` hands the body back. `h`/`j`/`k`/`l` are NOT motion here — a tagline is a place in the LAYOUT, so they run the same `Left`/`Down`/`Up`/`Right` builtins and land on the neighbouring pane's TAGLINE, still in normal mode (nothing that way = stay put); the arrows keep the in-tag motion | helix `:` is command mode (section C); pardes' commands are acme words that live in the tag. `tag_col`/`tag_anchor` are columns of the RENDERED tag (prefix ++ tail) — one coordinate space, so the live mode+path prefix is selectable, yankable and executable, while every edit op (typing, `Backspace`, `i`/`a`/`I`/`A`) measures from the first editable column and is inert inside it | pardes-specific | +| `Ctrl-w` + `h/j/k/l`/arrows | directional pane focus prefix — normal/tty modes only | pardes' own window handling (helix window mode skipped, section C). Runs the SAME `Left`/`Down`/`Up`/`Right` builtins `SPC w h/j/k/l` runs; kept alongside the leader because a pane in raw **tty** mode never sees `SPC` (the shell owns it), so this is the only keyboard way out of one. Insert mode owns `Ctrl-w` = delete-word-back, so a tag being TYPED into swallows it; from a tag in normal mode (`:`) it moves focus to the neighbour's BODY, while the bare letters `h/j/k/l` there move to its TAGLINE (next row) | pardes-specific | | `Alt-n` | new terminal below (any mode) | shadows helix `Alt-n` TS sibling-select — skipped anyway (tree-sitter) | pardes-specific | | `Alt-c` | move active terminal to a fresh column (any mode) | helix `Alt-c` is change-noyank; the pardes window op wins (do-not-touch contract). `Alt-d` + `i` covers the behavior | waived (`alt-c-window-op`) | | `Space` (normal, body) | the pardes LEADER: a key path from here runs a BUILTIN with no arguments — the same builtins the topbar and the tags hold. `SPC k` Kill, `SPC d` Del, `SPC f s` Save, `SPC f f` Find (fd over the pane's directory, results into `+Search`), `SPC h t` Tutor, `SPC c n`/`SPC c d` Newcol/Delcol, `SPC t d/c/n/r` Debug/Colors/NextColor/Crt, `SPC t p/l/a` Petscii/Palette/Ascii (an image pane's renderer toggles — the words its tag used to spell out, so the bar stays a bar), `SPC s d`/`SPC s r` Dump/Restore, `SPC w h/j/k/l` Left/Down/Up/Right (directional pane focus — the `Ctrl-w` prefix's four moves as builtins), `SPC w t` Toggleterm (focus hops between the most recently focused document — file, image or `+Search`/`+Help` buffer — and the most recently focused terminal; the same key hops back). `?` at ANY depth opens the Help builtin listing what the prefix can still reach (`SPC ?` = all, `SPC h ?` = the docs group), into a `+Help` output buffer. The pending path shows at the right edge of the active pane's tag; Esc — or any unmapped key — abandons it | helix spends Space on pickers/LSP (section C); pardes has neither, and acme's builtins are what a leader is for. Enum-derived: the paths, the Help lines and the execute dispatch all fold out of one `Builtin` enum at comptime. Normal mode on a BODY only — tags are always insert, tty keys belong to the program | pardes-specific | @@ -230,7 +231,7 @@ text. | `s S Alt-s`, `Alt-minus Alt-_`, `& _`, `, Alt-,`, `C Alt-C`, `( ) Alt-( Alt-)`, `K Alt-K`, `Alt-:` | regex select/split, merge, align/trim, rotate, keep/remove, copy-to-line, ensure-forward | multicursor machinery — pardes has exactly one selection | | `Alt-J` | join + select the inserted space | marginal over `J`; multicursor-flavored | | `\|` `Alt-\|` `!` `Alt-!` `$` | shell pipe/insert/append/keep | shell — pardes executes via Tab / middle-click chords instead | -| `:` | command mode | side-effects/file-ops — pardes builtins live in the tag | +| `:` | command mode | side-effects/file-ops — pardes builtins live in the tag, and `:` is bound to focusing it (section A) | | `gf` | goto file under selection | covered by pardes Enter-look | | `Ctrl-c` | toggle comments | language-dependent | | `Ctrl-z` | suspend | pardes IS the terminal multiplexer | diff --git a/src/pardes.zig b/src/pardes.zig index 31757732..6df520ce 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -704,6 +704,12 @@ pub const Pane = struct { /// the body mode a tag edit hijacked (tags are always insert); terminals /// restore it on exit so clicking the tag never changes the pane's mode tag_mode: Mode = .normal, + /// THE tag coordinate space: byte columns into the WHOLE rendered tag, + /// prefix ++ tail (tagText). One space, no conversions — the renderer, the + /// mouse and every motion speak it directly. The prefix is live chrome, so + /// it is selectable/yankable/executable but READ-ONLY: every edit op + /// measures from `edit0` (= tagPrefix().len, the first editable column) and + /// does nothing left of it. tag_col: u16 = 0, tag_anchor: u16 = 0, ed_undo: std.ArrayList(EditSnap) = .empty, @@ -838,6 +844,10 @@ const Drag = union(enum) { border_v: struct { left_col: usize, cur_x: u16 }, border_h: struct { col: usize, top_idx: usize, cur_y: u16 }, move: struct { id: usize, cur_x: u16, cur_y: u16 }, + /// a left sweep along a pane's TAG row: it drives the tag's own cursor and + /// selection (rendered-tag columns) rather than the body's block selection, + /// which is what makes a one-line tag select like a line of text + tag: struct { id: usize }, /// chorded: a 1-2/1-3 cut/paste chord fired during this left drag — /// the drag's own release is then inert select: struct { id: usize, button: Mouse.Button, chorded: bool = false }, @@ -1209,10 +1219,20 @@ pub const Pardes = struct { return pane_tail; } - /// focus the tag for editing, seeding the tail on first touch and placing - /// the cursor under the click (a column within the text area) - fn enterTagEdit(p: *Pardes, pane: *Pane, click_col: i32) void { - const pfx = p.tagPrefix(pane) catch return; + /// the tag exactly as it is rendered: prefix ++ tail. THE text tag_col and + /// tag_anchor index, so the renderer, the mouse, the motions and the chord + /// all read the same bytes at the same columns. + fn tagText(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) ![]u8 { + return std.fmt.allocPrint(arena, "{s}{s}", .{ try p.tagPrefix(pane), curTail(pane) }); + } + + /// focus the tag for editing, seeding the tail on first touch and parking + /// the cursor at `col` — a column of the RENDERED tag (see tag_col). + /// NEGATIVE means the first EDITABLE column, which is where `:` and a + /// tagline hop land: a place no click can name, so it needs no sentinel of + /// its own and the callers need no prefix length. + fn enterTagEdit(p: *Pardes, pane: *Pane, col: i32) void { + const edit0: i32 = @intCast((p.tagPrefix(pane) catch return).len); if (!pane.tag_init) { pane.tag_tail.appendSlice(p.gpa, curTail(pane)) catch return; pane.tag_init = true; @@ -1223,8 +1243,8 @@ pub const Pardes = struct { // a one-line tag has no use for normal mode: always insert pane.mode = .insert; pane.pending = 0; - const cc = std.math.clamp(click_col - @as(i32, @intCast(pfx.len)), 0, @as(i32, @intCast(pane.tag_tail.items.len))); - pane.tag_col = @intCast(cc); + const end = edit0 + @as(i32, @intCast(pane.tag_tail.items.len)); + pane.tag_col = @intCast(if (col < 0) edit0 else std.math.clamp(col, 0, end)); } fn exitTagEdit(pane: *Pane) void { @@ -1245,28 +1265,27 @@ pub const Pardes = struct { return .{ .lo = @min(a, c), .hi = @max(a, c) }; } - /// the text a tag Enter/Tab chord acts on: the char selection, else the - /// file-ish word under the cursor + /// the text a tag Enter/Tab chord (and `y`) acts on: the char selection, + /// else the file-ish word under the cursor. Over the WHOLE rendered tag, so + /// the path is a word like any other — Enter on it looks it. fn tagChordText(p: *Pardes, pane: *Pane) ?[]const u8 { - const arena = p.scratch.allocator(); - const tail = pane.tag_tail.items; + const text = p.tagText(p.scratch.allocator(), pane) catch return null; if (pane.tag_sel) { const b = tagSelBounds(pane); - const hi = @min(b.hi + 1, tail.len); - if (hi > b.lo) return arena.dupe(u8, tail[b.lo..hi]) catch null; - return null; + const hi = @min(b.hi + 1, text.len); + return if (hi > b.lo) text[b.lo..hi] else null; } - const col: usize = @min(@as(usize, pane.tag_col), tail.len); + const col: usize = @min(@as(usize, pane.tag_col), text.len); var lo = col; - while (lo > 0 and look.isFileChar(tail[lo - 1])) lo -= 1; + while (lo > 0 and look.isFileChar(text[lo - 1])) lo -= 1; var hi = col; - while (hi < tail.len and look.isFileChar(tail[hi])) hi += 1; - if (hi > lo) return arena.dupe(u8, tail[lo..hi]) catch null; - return null; + while (hi < text.len and look.isFileChar(text[hi])) hi += 1; + return if (hi > lo) text[lo..hi] else null; } - /// tag-edit key dispatch: a modal one-line editor over the tail, sharing - /// the pane's mode. Newlines are always dropped. + /// tag-edit key dispatch: a modal one-line editor over the rendered tag, + /// sharing the pane's mode — the cursor moves over all of it, edits reach + /// only the tail. Newlines are always dropped. fn tagInsertKey(p: *Pardes, pane: *Pane, key: Key) void { if (is(key, Key.escape)) { exitTagEdit(pane); // the tag is ALWAYS insert; Esc leaves it @@ -1281,35 +1300,40 @@ pub const Pardes = struct { } return; } + // the prefix is live chrome, not text you own: the cursor may sit in it + // (that is how the path selects), but every edit below is measured from + // the first EDITABLE column and simply does nothing to the left of it. + const edit0: u16 = @intCast((p.tagPrefix(pane) catch return).len); + const end: u16 = edit0 + @as(u16, @intCast(pane.tag_tail.items.len)); if (key.text.len > 0) { + if (pane.tag_col < edit0) return; // typing inside the path: inert for (key.text) |ch| { if (ch == '\n' or ch == '\r') continue; // one-line tag - pane.tag_tail.insert(p.gpa, pane.tag_col, ch) catch return; + pane.tag_tail.insert(p.gpa, pane.tag_col - edit0, ch) catch return; pane.tag_col += 1; } return; } switch (key.cp) { - Key.backspace => if (pane.tag_col > 0) { - _ = pane.tag_tail.orderedRemove(pane.tag_col - 1); + Key.backspace => if (pane.tag_col > edit0) { + _ = pane.tag_tail.orderedRemove(pane.tag_col - edit0 - 1); pane.tag_col -= 1; }, Key.left => if (pane.tag_col > 0) { pane.tag_col -= 1; }, - Key.right => if (pane.tag_col < pane.tag_tail.items.len) { + Key.right => if (pane.tag_col < end) { pane.tag_col += 1; }, Key.home => pane.tag_col = 0, - Key.end => pane.tag_col = @intCast(pane.tag_tail.items.len), + Key.end => pane.tag_col = end, else => {}, } } - /// write a helix range back onto the tag cursor + selection: the tail's - /// one-line mirror of setPaneRange. - fn setTagRange(pane: *Pane, r: modal.HxRange) void { - const text = pane.tag_tail.items; + /// write a helix range back onto the tag cursor + selection: the rendered + /// tag's one-line mirror of setPaneRange. + fn setTagRange(pane: *Pane, text: []const u8, r: modal.HxRange) void { const lo = @min(r.anchor, r.head); const hi = @max(r.anchor, r.head); pane.tag_col = @intCast(modal.hxCursor(text, r)); @@ -1318,10 +1342,12 @@ pub const Pardes = struct { } /// normal mode ON the tag — where `:` lands. The body's own helix motions - /// with the tail as a one-line document, plus insert entry and the acme - /// chords; editing keys stay in insert (`i` then type, like the mouse path). + /// with the WHOLE rendered tag as a one-line document (so the path selects + /// like any other text, and Enter on it looks it), plus insert entry and + /// the acme chords; editing keys stay in insert (`i` then type, like the + /// mouse path) and never reach left of `edit0`. fn tagNormalKey(p: *Pardes, pane: *Pane, key: Key) void { - const text = pane.tag_tail.items; + const text = p.tagText(p.scratch.allocator(), pane) catch return; const cur: usize = @min(@as(usize, pane.tag_col), text.len); // Esc abandons the command line: back to the body, tail kept as text if (is(key, Key.escape)) return exitTagEdit(pane); @@ -1337,21 +1363,57 @@ pub const Pardes = struct { p.actOnSelection(btn, id, txt, null); return; } - // insert entry (one line, so I/A are the tail's ends) + // y — yank what the chord would run: the selection, else the word under + // the cursor. The path is selectable, so this is how you copy it out. + if (is(key, 'y')) { + if (p.tagChordText(pane)) |txt| p.setYank(txt); + return; + } + // insert entry (one line, so I/A are the tail's ends). The prefix is + // read-only, so entering insert inside it parks at the first editable + // column instead — you can never be typing into the path. if (is(key, 'i') or is(key, 'a') or is(key, 'I') or is(key, 'A')) { - pane.tag_col = @intCast(switch (key.cp) { + const edit0: usize = (p.tagPrefix(pane) catch return).len; + pane.tag_col = @intCast(@max(edit0, switch (key.cp) { 'a' => modal.nextGrapheme(text, cur), 'I' => 0, 'A' => text.len, else => cur, - }); + })); pane.tag_sel = false; pane.mode = .insert; return; } - const target: ?usize = if (is(key, 'h') or is(key, Key.left)) + // h/j/k/l — a tagline is a place in the LAYOUT, so the four letters walk + // it: focus the neighbour and land on ITS tagline, still in normal mode, + // so tag-to-tag navigation never drops through a body. Runs the SAME + // Left/Down/Up/Right builtins `SPC w h/j/k/l` and `Ctrl-w` run — one + // focusDir. The ARROWS below keep the in-tag grapheme motion, so the + // letters cost nothing. Nothing in that direction = stay put, tag and + // all: focusDir left `active` alone, so there is nothing to undo. + const dir: ?Builtin = if (is(key, 'h')) + .Left + else if (is(key, 'j')) + .Down + else if (is(key, 'k')) + .Up + else if (is(key, 'l')) + .Right + else + null; + if (dir) |d| { + const from = p.active; + p.runBuiltin(d, from, ""); + if (p.active == from) return; + const dest = p.panes[p.active] orelse return; + exitTagEdit(pane); // the pane we left is fully out of its tag... + p.enterTagEdit(dest, -1); // ...and the tail's start is the new park + if (dest.tag_edit) dest.mode = .normal; + return; + } + const target: ?usize = if (is(key, Key.left)) modal.prevGrapheme(text, cur) - else if (is(key, 'l') or is(key, Key.right)) + else if (is(key, Key.right)) modal.nextGrapheme(text, cur) else if (is(key, '0') or is(key, Key.home)) 0 @@ -1361,7 +1423,7 @@ pub const Pardes = struct { modal.firstNonWs(text) else null; - if (target) |t| return setTagRange(pane, .{ .anchor = t, .head = t }); + if (target) |t| return setTagRange(pane, text, .{ .anchor = t, .head = t }); // word motions select their traversed span, exactly like the body's const wt: ?modal.WordTarget = if (is(key, 'w')) .next_word_start @@ -1379,18 +1441,14 @@ pub const Pardes = struct { null; if (wt) |t| { const range: modal.HxRange = .{ .anchor = cur, .head = modal.nextGrapheme(text, cur) }; - return setTagRange(pane, modal.hxWordMove(text, range, 1, t)); + return setTagRange(pane, text, modal.hxWordMove(text, range, 1, t)); } } - /// d/c: delete the selection (or the char under the cursor), yanking it; - /// c then enters insert /// tag + '\n' + body: the full selectable pane text (row 0 = the tag) fn paneText(p: *Pardes, pane: *Pane) ![]u8 { const arena = p.scratch.allocator(); - const pfx = try p.tagPrefix(pane); - const body = try p.bodyText(arena, pane); - return std.fmt.allocPrint(arena, "{s}{s}\n{s}", .{ pfx, curTail(pane), body }); + return std.fmt.allocPrint(arena, "{s}\n{s}", .{ try p.tagText(arena, pane), try p.bodyText(arena, pane) }); } /// block-selected text, newline-joined per row; reads the rendered screen @@ -2395,7 +2453,7 @@ pub const Pardes = struct { // hands the body back (`:w<Tab>` = Save). Always the start, not the // remembered tag column, so `:w` is the same keystrokes every time. if (is(key, ':')) { - p.enterTagEdit(pane, 0); + p.enterTagEdit(pane, -1); if (pane.tag_edit) pane.mode = .normal; return; } @@ -2435,7 +2493,9 @@ pub const Pardes = struct { pane.tag_sel = false; pane.mode = .insert; pane.pending = 0; - pane.tag_col = @intCast(pane.tag_tail.items.len); + // tag_col is a RENDERED-tag column, so the end of the tail is past the + // prefix (search_at stays a tail offset — it slices tag_tail) + pane.tag_col = @intCast((p.tagPrefix(pane) catch return).len + pane.tag_tail.items.len); } /// Enter on an armed input: the pattern is everything past the marker's @@ -3970,9 +4030,14 @@ pub const Pardes = struct { p.active = id; pane.scrollBy(-(@as(i32, mrow) - @as(i32, r.y + BOX_H))); } else if (mrow < r.y + BOX_H) { - // left click on the tag row: edit the tail in place + // left press on the tag row: focus the tag AT that + // column (a rendered-tag column — the mouse and the tag + // agree, no clamp into the tail) and anchor a sweep, so + // dragging selects any span of it, path included p.active = id; p.enterTagEdit(pane, @as(i32, mcol) - @as(i32, r.x + GUTTER)); + pane.tag_anchor = pane.tag_col; + p.drag = .{ .tag = .{ .id = id } }; } else { p.active = id; exitTagEdit(pane); // clicking the body leaves tag editing @@ -4070,6 +4135,16 @@ pub const Pardes = struct { pane.sel[b].r1 = @as(i32, mrow) - @as(i32, r.y); } }, + .tag => |d| { + // the tag cursor follows the mouse across the rendered tag; the + // row is ignored (a tag is one line) and the anchor stays where + // the press put it, so this is the mouse's `v` + const pane = p.panes[d.id] orelse return; + const end: i32 = @intCast((p.tagPrefix(pane) catch return).len + pane.tag_tail.items.len); + const c = @as(i32, mcol) - @as(i32, p.rects[d.id].x + GUTTER); + pane.tag_col = @intCast(std.math.clamp(c, 0, end)); + pane.tag_sel = pane.tag_col != pane.tag_anchor; + }, .none => {}, } } @@ -4159,6 +4234,7 @@ pub const Pardes = struct { if (txt) |tx| p.actOnSelection(s.button, s.id, tx, arg); } }, + .tag => {}, // dragUpdate already left the tag cursor + selection set .none => {}, } } @@ -4987,8 +5063,7 @@ pub const Pardes = struct { for (p.panes, 0..) |slot, id| { const pane = slot orelse continue; slot_to_pane[id] = panes.items.len; - const pfx = try p.tagPrefix(pane); - const tag = try std.fmt.allocPrint(arena, "{s}{s}", .{ pfx, curTail(pane) }); + const tag = try p.tagText(arena, pane); const body = try p.bodyText(arena, pane); const scroll: usize = @intCast(@max(0, pane.scroll())); const dp: dump.Pane = if (pane.file) |f| .{ @@ -5402,7 +5477,7 @@ pub const Pardes = struct { s.set(d.cur_x, placement.row, "▌", .{ .fg = .{ .rgb = th.lineno } }); } }, - .none, .select => { + .none, .select, .tag => { // hover hints on a pane's own trailing edge var c: usize = 0; while (c + 1 < p.ncol) : (c += 1) { @@ -5494,8 +5569,7 @@ pub const Pardes = struct { // tag: live prefix (mode + cwd/path) + editable tail, on a dim bar const tag_style: CellStyle = .{ .fg = .{ .rgb = th.tag_fg }, .bg = .{ .rgb = th.tag_bg } }; s.fill(tx, r.y, tw, BOX_H, .{ .bg = .{ .rgb = th.tag_bg } }); - const pfx = try p.tagPrefix(pane); - const tag = try std.fmt.allocPrint(arena, "{s}{s}", .{ pfx, curTail(pane) }); + const tag = try p.tagText(arena, pane); _ = s.print(tx, r.y, tw, tag, tag_style); // pending SPC leader path, on the ACTIVE pane's tag at its RIGHT edge: // the same rule as the `/` input — state you can see, nothing that @@ -5514,11 +5588,12 @@ pub const Pardes = struct { const w: u16 = @intCast(iw); if (w < tw) _ = s.print(tx + tw - w, r.y, w, ibuf[0..iw], tag_style); } - // tag-tail char selection highlight (helix v/x), inclusive [lo, hi] + // tag char selection highlight (helix v/x, or a tagline sweep), + // inclusive [lo, hi] — rendered-tag columns, so no conversion if (pane.tag_edit and pane.tag_sel) { const b = tagSelBounds(pane); - var col: usize = pfx.len + b.lo; - const end: usize = pfx.len + b.hi; + var col: usize = b.lo; + const end: usize = b.hi; while (col <= end and col < tw) : (col += 1) { const cell = s.at(tx + @as(u16, @intCast(col)), r.y); cell.default = false; @@ -5526,11 +5601,10 @@ pub const Pardes = struct { cell.style.fg = .{ .rgb = msel_fg }; } } - // cursor while editing the tail: prefix length + column within it + // cursor while editing the tag: a rendered-tag column, verbatim if (active and pane.tag_edit) { - const cx: usize = pfx.len + pane.tag_col; // bar while typing, block for `:` normal mode (same rule as a body) - if (cx < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = r.y, .bar = pane.mode == .insert }; + if (pane.tag_col < tw) s.cursor = .{ .x = tx + pane.tag_col, .y = r.y, .bar = pane.mode == .insert }; } // image pane: the picture fills the body — petscii glyph art into the diff --git a/src/tutor.txt b/src/tutor.txt index 39a869e2..c263015b 100644 --- a/src/tutor.txt +++ b/src/tutor.txt @@ -113,11 +113,23 @@ writes the current file to disk, Del closes the window. Clicking a tag edits it in insert mode: type straight in, Enter looks / Tab executes the word at the cursor, Esc hands focus back to the body. `:` from the - body focuses that same tag in NORMAL mode, parked at its start — vim's - command line with acme's words in it: `:w<Tab>` is w onto "Save", Tab - to execute it, and you land back in the body where you left off (the - chord keys are the same here as anywhere else; `i` if you would rather - type a command than walk to one). Terminal and image panes show "Del". + body focuses that same tag in NORMAL mode, parked at the first EDITABLE + column — vim's command line with acme's words in it: `:w<Tab>` is w + onto "Save", Tab to execute it, and you land back in the body where you + left off (the chord keys are the same here as anywhere else; `i` if you + would rather type a command than walk to one). Terminal and image panes + show "Del". + In that normal mode h/j/k/l do NOT move inside the tag: a tagline is a + place in the LAYOUT, so they focus the neighbouring window and land on + ITS tagline, still in normal mode — you walk the taglines of the screen + and never drop through a body. (Same moves as SPC w h/j/k/l and Ctrl-w + h/j/k/l; nothing that way = you stay put.) The ARROWS are the in-tag + motion, and w/b/e/0/$/^ still walk the tag's own words. + The MODE + path part of a tag is live chrome, so it cannot be edited — + but it CAN be selected, by motion or by dragging across it: 0 then W/E + selects the path, `y` yanks it, Enter looks it. Typing or backspacing + with the cursor inside it does nothing; edits only ever reach the words + you own, to the right of the path. "Delcol" still exists as a command: type it in a tag or body and execute it to close the whole column. A SEPARATE bar across the top of the screen holds the window-agnostic builtins: @@ -523,7 +535,9 @@ the input is empty, mid-text otherwise) KEYS (helix): h j k l w b e 0 $ ^ gg G v x d c y p i a I A o O u undo U redo Enter=look Tab=execute - : = the tag as a command line (motions, Enter, back) + : = the tag as a command line (arrows/words move in it, + hjkl walk to the neighbouring TAGLINE, y/Enter act on + the selection; the mode+path selects but never edits) zt zz zb Ctrl-d/u Ctrl-f Ctrl-w hjkl Alt-n/c SPC = the leader: a key path runs a builtin (SPC ? lists them; SPC k Kill, SPC d Del, SPC f s Save, SPC f f Find, diff --git a/test/snapshots/images.snap b/test/snapshots/images.snap index 9c510bed..e31d57ec 100644 --- a/test/snapshots/images.snap +++ b/test/snapshots/images.snap @@ -36,9 +36,12 @@ key a stable 700 15000 snap no-ascii # the tag takes an edit like any other pane's now — nothing live left in it to -# protect, so the image early-return in enterTagEdit is gone -press left 20 2 -release left 20 2 +# protect, so the image early-return in enterTagEdit is gone. Col 42 is the +# FIRST EDITABLE column (right after "img <path>"): a tag column is a column of +# the whole rendered tag, and the live prefix is read-only, so a click at col 20 +# would land in the path and the typing below would be inert. +press left 42 2 +release left 42 2 stable 400 5000 text Zork stable 400 5000 diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden new file mode 100644 index 00000000..2b00eba5 --- /dev/null +++ b/test/snapshots/tagnav.golden @@ -0,0 +1,372 @@ +== snap start grid=100x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap colon grid=100x30 cursor=39,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap right-tag grid=100x30 cursor=83,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap left-tag grid=100x30 cursor=39,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap down-tag grid=100x30 cursor=33,16 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap up-tag grid=100x30 cursor=39,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap up-edge grid=100x30 cursor=39,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap arrows-still-motion grid=100x30 cursor=41,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap back-to-body grid=100x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap tty-on grid=100x30 cursor=54,4 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del TTY /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha $ ls +| 2 beta w.txt +| 3 gamma $ +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap tty-tag grid=100x30 cursor=83,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha +| 2 beta w.txt +| 3 gamma +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| +== snap tty-restored grid=100x30 cursor=54,4 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del TTY /tmp/pardes-snap/tagnav/cwd Del +| 1 alpha $ ls +| 2 beta w.txt +| 3 gamma $ +| +| +| +| +| +| +| +| +| +| +| +| NOR /tmp/pardes-snap/tagnav/cwd Del +| +| w.txt +| +| +| +| +| +| +| +| +| +| +| diff --git a/test/snapshots/tagnav.snap b/test/snapshots/tagnav.snap new file mode 100644 index 00000000..6ba86c08 --- /dev/null +++ b/test/snapshots/tagnav.snap @@ -0,0 +1,75 @@ +# h/j/k/l ON A TAGLINE move focus BETWEEN taglines. A tag is a place in the +# LAYOUT, not just a line of text, so the four letters walk the layout and land +# on the neighbour's TAG (top row of the window), still in normal mode — you +# walk the taglines and never drop through a body. They run the SAME +# Left/Down/Up/Right builtins `SPC w h/j/k/l` and `Ctrl-w h/j/k/l` run: one +# focusDir, three routes to it. The ARROWS keep the in-tag grapheme motion, so +# no capability was traded away. +# +# Evidence is the `cursor=` header (the ACTIVE pane's cursor, which while a tag +# is focused sits on THAT pane's tag row) plus the tag text itself: a focused +# tag renders its owner's mode, so NOR means the tag is in normal mode and the +# pane we left is back in its body mode. +file w.txt alpha\nbeta\ngamma +start 30 100 w.txt +wait 8000 Kill Newcol +wait 8000 w.txt +stable 700 20000 +# Alt-n: a shell below the file, so the left column has two rows and every +# direction has somewhere to go +key a-n +stable 700 15000 +key space w k +stable 400 5000 +snap start +# `:` -> the file's OWN tagline, normal mode, parked at the tail's start +key : +stable 400 5000 +snap colon +# l -> right, onto the launch shell's TAGLINE (not its body) +key l +stable 400 5000 +snap right-tag +# h -> back left onto the file's tagline +key h +stable 400 5000 +snap left-tag +# j -> down onto the tagline of the shell under the file +key j +stable 400 5000 +snap down-tag +# k -> back up onto the file's +key k +stable 400 5000 +snap up-tag +# k again: nothing above the file. focusDir leaves `active` alone, so the move +# is a plain no-op and the tag KEEPS focus — you stay exactly where you were. +key k +stable 400 5000 +snap up-edge +# the arrows are still in-tag motion: two rights walk two columns of the tail +key right right +stable 400 5000 +snap arrows-still-motion +key esc +settle 700 +stable 400 5000 +snap back-to-body +# A TERMINAL MUST NOT LOSE ITS TTY MODE. Put the launch shell in raw tty mode +# (Ctrl-b), come back, then hop onto its tagline and off again: exitTagEdit +# restores the body mode the tag hijacked, so its prefix reads TTY again. +key c-w l +stable 400 5000 +key c-b +stable 700 15000 +snap tty-on +key c-w h +stable 400 5000 +key : +key l +stable 400 5000 +snap tty-tag +key esc +settle 700 +stable 400 5000 +snap tty-restored diff --git a/test/snapshots/tagpath.golden b/test/snapshots/tagpath.golden new file mode 100644 index 00000000..0b073a8c --- /dev/null +++ b/test/snapshots/tagpath.golden @@ -0,0 +1,310 @@ +== snap start grid=100x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del +| 1 line 1 +| 2 line 2 c.txt +| 3 line 3 +| 4 line 4 +| 5 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +== snap colon grid=100x30 cursor=40,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del +| 1 line 1 +| 2 line 2 c.txt +| 3 line 3 +| 4 line 4 +| 5 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +== snap col0 grid=100x30 cursor=2,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del +| 1 line 1 +| 2 line 2 c.txt +| 3 line 3 +| 4 line 4 +| 5 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +== snap sel-nor grid=100x30 cursor=5,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del +| 1 line 1 +| 2 line 2 c.txt +| 3 line 3 +| 4 line 4 +| 5 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +== snap sel-path grid=100x30 cursor=39,1 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del +| 1 line 1 +| 2 line 2 c.txt +| 3 line 3 +| 4 line 4 +| 5 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +== style sel-path +|0: 0-37 #949494,#121212, 38-99 d,#121212, +|1: 0-1 d,#626262, 2-4 #949494,#121212, 5-39 #d8c8a8,#4a3a1e, 40-48 #949494,#121212, 49-49 d,#121212, 50-51 d,#262626, 52-87 #949494,#121212, 88-99 d,#121212, +|2: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|3: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|4: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|5: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|6: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|7: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|8: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|9: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|10: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|11: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|12: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|13: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|14: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|15: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|16: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|17: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|18: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|19: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|20: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|21: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|22: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|23: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|24: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|25: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|26: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|27: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|28: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|29: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +== snap yanked-path grid=100x30 cursor=42,2 +|Kill Newcol Tutor Debug NextColor Dump +| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del +| 1 l /tmp/pardes-snap/tagpath/cwd/c.txtine 1 +| 2 line 2 c.txt +| 3 line 3 +| 4 line 4 +| 5 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +== snap drag-cross grid=100x30 cursor=45,1 +|Kill Newcol Tutor Debug NextColor Dump +| INS /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del +| 1 l /tmp/pardes-snap/tagpath/cwd/c.txtine 1 +| 2 line 2 c.txt +| 3 line 3 +| 4 line 4 +| 5 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +== style drag-cross +|0: 0-37 #949494,#121212, 38-99 d,#121212, +|1: 0-1 d,#626262, 2-10 #949494,#121212, 11-45 #d8c8a8,#4a3a1e, 46-48 #949494,#121212, 49-49 d,#121212, 50-51 d,#262626, 52-87 #949494,#121212, 88-99 d,#121212, +|2: 0-1 d,#444444, 2-6 #626262,#080808, 7-47 #bdbdbd,#080808, 48-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|3: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|4: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|5: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|6: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|7: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|8: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|9: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|10: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|11: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|12: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|13: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|14: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|15: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|16: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|17: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|18: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|19: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|20: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|21: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|22: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|23: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|24: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|25: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|26: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|27: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|28: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +|29: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808, +== snap path-inert grid=100x30 cursor=11,1 +|Kill Newcol Tutor Debug NextColor Dump +| INS /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del +| 1 l /tmp/pardes-snap/tagpath/cwd/c.txtine 1 +| 2 line 2 c.txt +| 3 line 3 +| 4 line 4 +| 5 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| diff --git a/test/snapshots/tagpath.snap b/test/snapshots/tagpath.snap new file mode 100644 index 00000000..61181dd5 --- /dev/null +++ b/test/snapshots/tagpath.snap @@ -0,0 +1,69 @@ +# THE PATH IN A TAG IS SELECTABLE, NOT EDITABLE. +# tag_col/tag_anchor index the WHOLE rendered tag (prefix ++ tail) — one +# coordinate space, no conversions: the renderer prints the cursor at tag_col +# verbatim and a mouse column IS a tag column. So motions and sweeps reach the +# mode indicator and the path, a selection over them yanks and executes like +# any other text, and the ONLY thing fenced off is EDITING: every edit op +# measures from the first editable column (tagPrefix().len) and does nothing to +# the left of it. +lines c.txt 4 +start 30 100 c.txt +wait 8000 Kill Newcol +wait 8000 c.txt +stable 700 20000 +snap start +# --- keyboard: select part of the PATH --- +# `:` parks at the tail's start (the first EDITABLE column) — unchanged, that +# is what makes `:w<Tab>` the same two keystrokes every time +key : +stable 400 5000 +snap colon +# 0 goes to column 0 of the RENDERED tag: the "N" of NOR, a column the keyboard +# could not name at all before +key 0 +stable 400 5000 +snap col0 +# W: the long word it traverses, landing the cursor on the space after "NOR" +key W +stable 400 5000 +snap sel-nor +# E: extend to the end of the next long word — the style capture shows ONE +# highlighted run from that space through the last char of the path +key E +stable 400 5000 +snap sel-path +snapstyle sel-path +# y yanks the selection. Esc hands the body back; p pastes the register into +# the file, which is the only way a snapshot can see what was yanked. +key y +key esc +settle 700 +stable 400 5000 +key p +stable 400 5000 +snap yanked-path +# --- mouse: a sweep across the prefix/tail boundary --- +# a left press on a tagline anchors a TAG sweep (not the body's block +# selection), so the drag paints one run of the tag row. Press at col 12 +# (inside the path), release at col 46 (just past "Save" in the tail): the +# highlight crosses the boundary — screen col 40, where the tail starts — in +# one piece, which is the whole point of there being one coordinate space. +press left 12 2 +drag left 30 2 +drag left 46 2 +release left 46 2 +stable 400 5000 +snap drag-cross +snapstyle drag-cross +# --- the path is inert to editing --- +# a plain click INSIDE the path leaves the cursor there (no clamp into the +# tail), in insert mode like any tag click — and then typing and backspace do +# NOTHING: the tag row below is byte-identical to the one in `yanked-path`. +press left 12 2 +release left 12 2 +stable 400 5000 +text ZZNOPE +key bs +key bs +stable 400 5000 +snap path-inert |
