From be404e0d4f91548af6ed600b2fd2027ad8a041c7 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 31 Jul 2026 01:47:40 -0300 Subject: focus history is a stack of locations, and the Jumplist is that same stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One container, not two: focus_hist (a stack of pane ids rebuilt every sync) is now jumps[] + a current pointer, and the +Jumps buffer is a RENDERING of that array — nothing copies it, nothing shadows it. prevFocus, Toggleterm, Look's directory order, Back/Forward, Last and Jumplist all walk the one list. A pane id is reused, so a location that only remembered an id would retarget after a respawn: panes now carry a monotonic serial and an entry whose slot holds a different serial is dead. trackJump compacts those out and fixes the pointer in the same pass. The push rule lives in ONE place and says: a location is worth remembering when you cannot see it any more — a different pane, or more than a bodyful of rows away in the same one. So hjkl never grows the list and 100G, a search hit and a goto-definition do. Ctrl-o/Ctrl-i walk it, SPC j j toggles the last two, SPC j l lists them. Ctrl-i IS Tab on a legacy host, where the binding simply never fires and Tab-executes is untouched; kitty reports them apart. SPC j o/i work anywhere. Two goldens moved, both the SPC ? Help listing gaining four rows. --- docs/design.typ | 2 +- next-steps.txt | 2 +- src/builtins.zig | 66 +++++- src/config.zig | 33 +++ src/output_pane.zig | 71 ++++++ src/pardes.zig | 173 +++++++++++--- test/snapshots/jumps.golden | 527 ++++++++++++++++++++++++++++++++++++++++++ test/snapshots/jumps.snap | 85 +++++++ test/snapshots/leader.golden | 28 +-- test/snapshots/ttyhelp.golden | 60 ++--- 10 files changed, 965 insertions(+), 82 deletions(-) create mode 100644 test/snapshots/jumps.golden create mode 100644 test/snapshots/jumps.snap diff --git a/docs/design.typ b/docs/design.typ index 118459e1..7a9cc5c7 100644 --- a/docs/design.typ +++ b/docs/design.typ @@ -206,7 +206,7 @@ click scrolls up-to-row (right button: down), border/move drags. Middle: execute — no-drag expands to file-ish word (alnum `.-+/:@_~`); builtin or send to shell; does not focus. Right: look — peel `:NNN`, resolve against the clicked pane's dir (`/proc/pid/cwd`, or a file's dirname) and, if that fails, -against every other live pane's, most-recently-focused first (focus_hist +against every other live pane's, most-recently-focused first (the jump stack backwards, duplicate dirs skipped; absolute words try one dir and stop) so a relative name is openable from any window that can see it; dir → shell + `ls` in source column (dedup by cwd), file → file pane at line (dedup by path, diff --git a/next-steps.txt b/next-steps.txt index 29073998..a230beca 100644 --- a/next-steps.txt +++ b/next-steps.txt @@ -9,5 +9,5 @@ - There's some bug when rendering from a ssh session - the screen gets dirty with old chars when scrolling. - There's some bad performance when dealing with specially large files or long lines; write a perf harness for this and optimize the performance. + The Look and Exec should be regular semantic entities, i.e. their only special feature should be the keys/shortcuts they're assigned so we could execute Look main.zig and it would call the regular look on it, but also we could Look some string like @`ls -la` (you can change the syntax, but this also depends on that syntax change so it's centralized and easy to change later). This basically will allow you to make some big refactorings and cut a lot of code (hopefully) and also allow some funny things like @`Look .` (basically allowing nested things like this for no good reason other than, it's elegant and it's fun!) -- We'll make the focus history a meaningful and strong semantic entity: instead of being a stack of Panes, it'll be a stack of locations. This will allow us to implement vim's ctrl-i and ctrl-o that will just move the current focus up or down on that stack. there'll be also a new builtin that will toggle the focus of the latest panes, basically moving the current focus by changing the current pointer to the location on the stack. The Jumplist (new builtin) will be an Output pane with each line being a location that can be Looked, n/N should work. ++ We'll make the focus history a meaningful and strong semantic entity: instead of being a stack of Panes, it'll be a stack of locations. This will allow us to implement vim's ctrl-i and ctrl-o that will just move the current focus up or down on that stack. there'll be also a new builtin that will toggle the focus of the latest panes, basically moving the current focus by changing the current pointer to the location on the stack. The Jumplist (new builtin) will be an Output pane with each line being a location that can be Looked, n/N should work. + The main pardes.zig file should hold the logic that applies to all kinds of panes and backends. Maybe right now the pane specifiticies are too intermingled. Maintain the panes logics into their own files, right now tty.zig exists but I don't think output panes live by themselves nor file panes on their own files. Also, each builtin command should be implemented as a struct, the current logic on switching on the enum should use some comptime logic to build the tagged union/enum from those structs now. Also I think it's time to create a few directories on @src now since some files are specific to some backends and others are part of the core, it should be clear just by looking at the directory structure (don't add more dir layers, just new dirs under src). diff --git a/src/builtins.zig b/src/builtins.zig index cf144d33..7e20a95b 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -351,13 +351,12 @@ pub const Right = struct { } }; -/// The file<->terminal hop. "Latest" is already recorded: focus_hist is the -/// MRU sync() rebuilds every update (most recent last), the same list a -/// closing pane hands focus back through — so this walks it instead of keeping -/// a second one. Which side is which: only a shell is a terminal; a file, an -/// image and an output buffer (+Search/+Help) are all DOCS you read, so -/// isTerminal is the whole test. Landing pushes this pane to the top of that -/// same history, which is why the hop back is the same key. +/// The file<->terminal hop. "Latest" is already recorded: the jump stack runs +/// oldest-first and a closing pane hands focus back through the same list — so +/// this walks it instead of keeping a second one. Which side is which: only a +/// shell is a terminal; a file, an image and an output buffer (+Search/+Help) +/// are all DOCS you read, so isTerminal is the whole test. Landing pushes this +/// pane onto that same history, which is why the hop back is the same key. pub const Toggleterm = struct { pub fn run(c: Ctx) void { const want_term = !c.pane.isTerminal(); @@ -369,10 +368,10 @@ pub const Toggleterm = struct { if (k != c.id and op.isTerminal() == want_term) t = k; } // ...but the most recently focused one wins - var i = c.p.nfocus; + var i = c.p.njumps; while (i > 0) { i -= 1; - const hid = c.p.focus_hist[i]; + const hid = c.p.jumps[i].pane; const hp = c.p.panes[hid] orelse continue; if (hid != c.id and hp.isTerminal() == want_term) { t = hid; @@ -386,6 +385,55 @@ pub const Toggleterm = struct { } }; +// ---- the jump group ---- + +// Where focus HAS BEEN, as three verbs and a list over the one stack pardes +// keeps (Pardes.jumps — see trackJump for what gets onto it). Builtins rather +// than bare key handlers for the same reason the four directions are: one +// implementation, reachable by chord, by `SPC j ...`, and by executing the +// word wherever it is written. + +/// Ctrl-o: one step back into the history. +pub const Back = struct { + pub fn run(c: Ctx) void { + c.p.jumpBy(-1); + } +}; + +/// Ctrl-i: one step forward again, up to wherever Back started. +pub const Forward = struct { + pub fn run(c: Ctx) void { + c.p.jumpBy(1); + } +}; + +/// vim's Ctrl-^: the pane you were in before this one, whichever it was — the +/// hop you press twice a minute and never want to count steps for. +/// +/// It does NOT move the stack cursor: it goes somewhere, so trackJump records +/// it like any other move, and that is exactly what makes it an involution — +/// after the hop, the pane you came from is the newest OTHER pane, so pressing +/// it again comes straight back. Back/Forward walk history; this one makes it. +pub const Last = struct { + pub fn run(c: Ctx) void { + var i = c.p.njumps; + while (i > 0) { + i -= 1; + const j = c.p.jumps[i]; + if (j.pane != c.id) return c.p.focusPaneLine(j.pane, j.line, j.col); + } + } +}; + +/// The same stack, as text you can read and click. Not a copy of it and not a +/// second list kept in step — the buffer is RENDERED from the stack when you +/// ask, the way +Search is rendered from a walk. +pub const Jumplist = struct { + pub fn run(c: Ctx) void { + output_pane.openJumps(c.p, c.id); + } +}; + // ---- the language group ---- // Reached as `SPC l ` — see leader_path for why the prefix diff --git a/src/config.zig b/src/config.zig index 394f9ca3..c4249270 100644 --- a/src/config.zig +++ b/src/config.zig @@ -123,6 +123,15 @@ pub const leader_path = std.EnumArray(Builtin, ?[]const u8).init(.{ .Up = "wk", .Right = "wl", .Toggleterm = "wt", + // the `j` JUMP group, its own letter rather than more of `w`: the window + // group moves focus by GEOMETRY (the pane left of this one), these move it + // by TIME (the pane I was in before). `o` and `i` are the letters of the + // chords that do the same thing, `jj` is the group's obvious verb, and + // `jl` is the list itself. + .Back = "jo", + .Forward = "ji", + .Last = "jj", + .Jumplist = "jl", // the two acme verbs: keys and buttons, no leader path — see above .Look = null, .Exec = null, @@ -180,6 +189,29 @@ pub const window_keys = [_]struct { letter: Chord, arrow: Chord, cmd: Builtin }{ pub const new_shell_below: []const Chord = &.{.{ .cp = 'n', .alt = true }}; pub const pane_to_new_column: []const Chord = &.{.{ .cp = 'c', .alt = true }}; +// ---- jumps ---- + +/// vim's Ctrl-o / Ctrl-i, walking the focus history back and forward. Global +/// in any mode, like the two Alt- ops above and for the same reason: getting +/// BACK has to work from inside a pane that owns its keys. +/// +/// CAREFUL, and this is why the table has a comment: **Ctrl-i is Tab**. On the +/// wire they are the same byte (0x09), so on a host that speaks only the +/// legacy encoding this binding never fires and 0x09 keeps meaning `exec_key` +/// below — which is the right way round, since Tab-executes is the older and +/// more used of the two. Where the host speaks the kitty keyboard protocol +/// (`CSI 105;5u`) the two are distinct keys and both work. Shift-Esc +/// (tty_toggle_alt) is already spelled on that same bet. +/// +/// Shaped like window_keys: the `cmd` column is what makes the chord +/// discoverable from the builtin as well as the other way round, and it is +/// what keeps ONE implementation — pressing the chord and executing the word +/// `Back` are the same call. +pub const jump_keys = [_]struct { chord: Chord, cmd: Builtin }{ + .{ .chord = .{ .cp = 'o', .ctrl = true }, .cmd = .Back }, + .{ .chord = .{ .cp = 'i', .ctrl = true }, .cmd = .Forward }, +}; + /// `j` off the topbar drops back onto a tagline — the mirror of the `k` that /// got you there (window_keys' letter, answered by tagNormalKey). pub const topbar_down: []const Chord = &.{.{ .cp = 'j' }}; @@ -368,6 +400,7 @@ pub const symbol_marker = " WsSymbols /"; /// these changes only what you read in a tag. pub const search_buffer = "+Search"; pub const help_buffer = "+Help"; +pub const jumps_buffer = "+Jumps"; pub const hover_buffer = "+Hover"; pub const lsp_buffer = "+Lsp"; diff --git a/src/output_pane.zig b/src/output_pane.zig index 45eb0931..8e08285f 100644 --- a/src/output_pane.zig +++ b/src/output_pane.zig @@ -20,6 +20,7 @@ const Pardes = pardes.Pardes; const Pane = pardes.Pane; const File = pardes.File; const file_pane = @import("file_pane.zig"); +const modal = @import("modal.zig"); const builtins = @import("builtins.zig"); const Builtin = builtins.Builtin(); const config = @import("config.zig"); @@ -110,6 +111,9 @@ pub fn traits(o: Origin) Traits { .search => .{ .name = config.search_buffer, .steps = true }, .cmd => |b| switch (b) { .Help => .{ .name = config.help_buffer }, + // the focus history, one location per row: not a search, but the + // same kind of list, so n/N walk it and a row is a look target + .Jumplist => .{ .name = config.jumps_buffer, .steps = true }, // Find (file names) and Grep (file contents) both list locations; // no other builtin opens a buffer, and the day one does it lands // here rather than in a call site. @@ -188,6 +192,73 @@ pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u return pane; } +/// The Jumplist builtin: the focus history (Pardes.jumps) written out as text, +/// one row per location, oldest first — the same `location text` shape every +/// results buffer here has, which is what buys n/N stepping and Look-on-a-row +/// for nothing: the leading word is an ordinary look target, and the ordinary +/// look path is what goes there. +/// +/// A RENDERING, never a second list. The rows are spelled from the stack at +/// the moment you ask and go stale the moment you jump, exactly like a search +/// result — which is also why this opens a fresh buffer per press instead of +/// refreshing one the way Help does: Help is a document, this is a snapshot. +/// +/// How a location is spelled is the rule runSearch already follows: a REAL +/// file names itself (its path is absolute, so the row resolves from any +/// pane's directory), and everything else — a terminal, an output buffer, an +/// image — has no file to point at and gets `@pN`. The trailing text is the +/// content line for anything holding text, else the pane's directory: enough +/// to recognise the place without opening it. +pub fn openJumps(p: *Pardes, id: usize) void { + const pane = p.panes[id] orelse return; + const arena = p.scratch.allocator(); + var out: std.ArrayList(u8) = .empty; + for (p.jumps[0..p.njumps]) |j| { + const jp = p.panes[j.pane] orelse continue; + var idbuf: [16]u8 = undefined; + const has_path = if (jp.file) |f| f.output == null else false; + const loc: []const u8 = if (has_path) + jp.file.?.path + else + std.fmt.bufPrint(&idbuf, config.pane_addr ++ "{d}", .{j.pane}) catch continue; + const what: []const u8 = if (jp.file) |f| + std.mem.trim(u8, modal.lineSlice(f.content, j.line -| 1), " \t\r") + else if (jp.image) |iv| + iv.path + else + jp.cwdSlice(); + // one line of a file can be the whole file: cut it, but never mid + // codepoint — a partial UTF-8 sequence reaches the renderer as a row + // and there is nothing sane for it to draw (grepText cuts the same way) + var cut = @min(what.len, 120); + while (cut > 0 and cut < what.len and what[cut] & 0xc0 == 0x80) cut -= 1; + // line 0 is a place with no spot in it (a shell whose cursor is still + // the program's): it is written WITHOUT the suffix, which is the same + // thing a bare path has always meant to a look. + const row = if (j.line == 0) + std.fmt.allocPrint(arena, "{s} {s}\n", .{ loc, what[0..cut] }) catch return + else + std.fmt.allocPrint(arena, "{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] }) catch return; + out.appendSlice(arena, row) catch return; + } + const content = p.gpa.dupe(u8, out.items) catch return; + const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice(); + const free = p.freeSlot() orelse { + p.gpa.free(content); + return; + }; + const np = open(p, free, dir, .{ .cmd = .Jumplist }, "", content) catch { + p.gpa.free(content); + return; + }; + p.placeDoc(id, free, np); + // focus stays with the pane that asked, exactly as it does after a search: + // n/N are read there, and they step the buffer they just armed. + p.active = id; + pane.search_pane = free; + pane.search_row = null; +} + /// The Help builtin: every builtin reachable from `prefix` (empty = all /// of them) with the keys that run it, written into an output buffer /// (acme's +Errors) — ordinary text, so the names in it are LIVE: middle- diff --git a/src/pardes.zig b/src/pardes.zig index 2365c33f..3bf32c82 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -44,9 +44,34 @@ pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config" // minimums — live in config.zig with everything else a user retargets. pub const MAX_PANES = 16; pub const MAX_COLS = 6; +/// how far back the jump stack remembers. Vim keeps 100; this is a session of +/// at most sixteen panes, so the depth that matters is "more visits than you +/// can hold in your head" and the oldest entry falls off the bottom. +pub const MAX_JUMPS = 64; pub const TOPBAR_H: u16 = 1; pub const BOX_H: u16 = 1; +/// A place the keyboard has been: a pane AND a spot in it, which is the whole +/// upgrade over the stack of bare pane ids this replaces — Ctrl-o can now +/// rewind WITHIN a pane, and a Jumplist row can name a line. +/// +/// It is SAFE against the pane it names dying: `serial` is the pane's own +/// identity, so an entry whose slot has since been handed to a different pane +/// reads as dead rather than silently retargeting itself at the newcomer, and +/// sync() drops it. What it does not do is outlive the pane — ponytail: a +/// location is a place in the SESSION, not on disk, so closing a file forgets +/// the entries pointing into it. To make Ctrl-o RE-OPEN a closed file, this +/// grows a path field and jumpBy looks it when the pane is gone. +pub const Loc = struct { + pane: u16, + serial: u32, + /// 1-based, both — this is the `path:LINE:COL` a look word spells, and + /// focusPaneLine takes exactly these. 0 = no spot, just the pane (see + /// trackJump: a shell whose cursor is still the program's). + line: u32, + col: u32, +}; + const pane_tail = " " ++ config.pane_builtins_str; const file_pane_tail = " " ++ config.file_pane_builtins_str; @@ -654,6 +679,11 @@ pub const Image = struct { pub const Pane = struct { vt: ghostty_vt.Terminal, stream: ghostty_vt.TerminalStream, + /// WHICH pane this is, for anything that outlives the pane: slots are + /// REUSED (freeSlot hands back the lowest free one), so a remembered id + /// alone can silently come to mean an unrelated pane. Handed out by + /// Pardes.next_serial and never reused. The one reader is the jump stack. + serial: u32 = 0, mode: Mode = .normal, vweight: f32 = 1, cols: u16, @@ -899,11 +929,21 @@ pub const Pardes = struct { col_w: [MAX_COLS]u16 = undefined, active: usize = 0, - /// keyboard focus history, least-recent first, `active` last: rebuilt each - /// sync (dead slots pruned, ids are reused). A closing pane hands focus - /// back to the last live one instead of whatever slot comes first. - focus_hist: [MAX_PANES]usize = undefined, - nfocus: usize = 0, + /// THE focus history: where the keyboard has BEEN, oldest first, and `jcur` + /// is the entry it is at NOW (so `jumps[jcur]` is always the live spot and + /// the entries past it are the ones Ctrl-i walks forward into). Maintained + /// in exactly one place — sync() — and read by everything that asks "where + /// was I": Back/Forward, Last, the Jumplist buffer, prevFocus when a pane + /// closes, Toggleterm, and the directory order a look resolves in. + /// + /// One list, not two. A jumplist kept beside a focus history is two things + /// to keep agreeing, and they would disagree the first time one of them + /// forgot a pane the other still names. + jumps: [MAX_JUMPS]Loc = undefined, + njumps: usize = 0, + jcur: usize = 0, + /// hands out Pane.serial; monotonic, never reused + next_serial: u32 = 0, theme_idx: usize = 0, colors_on: bool = true, show_debug: bool = false, @@ -1111,6 +1151,8 @@ pub const Pardes = struct { // crossterm apps (nushell, helix, fzf) block on the reply forever. pane.stream.handler.effects.write_pty = term_pane.ptyReport; pane.stream.handler.effects.device_attributes = term_pane.ptyDeviceAttrs; + p.next_serial += 1; + pane.serial = p.next_serial; p.panes[id] = pane; p.emit(.{ .spawn = .{ .pane = @intCast(id), .cwd = .from(cwd) } }); return pane; @@ -1128,6 +1170,8 @@ pub const Pardes = struct { .rows = p.screen_h, }; pane.stream = pane.vt.vtStream(); + p.next_serial += 1; + pane.serial = p.next_serial; p.panes[id] = pane; return pane; } @@ -1690,6 +1734,14 @@ pub const Pardes = struct { p.active = free; return; } + // the jump chords, global for the same reason: Ctrl-o has to get you + // out of wherever you are, including a pane in raw tty mode. Above the + // acme chords below, which is what makes Ctrl-i reachable at all — on + // a kitty-protocol host it arrives as its own key, and where it does + // not it IS Tab (0x09) and falls through to Exec, see config.jump_keys. + for (config.jump_keys) |jk| { + if (hit(key, &.{jk.chord})) return p.runBuiltin(jk.cmd, p.active, "", null); + } if (hit(key, config.pane_to_new_column)) { const f = p.layoutFindTerm(p.active).?; if (p.ncol < MAX_COLS and p.col_n[f.col] > 1) { @@ -4888,7 +4940,7 @@ pub const Pardes = struct { /// there (`col` likewise, 0 = line start): files recenter the view on it, /// terminals ride their scrollback to it. Both look targets that name a /// live pane land here — a path a pane already holds, and `@pN:LINE:COL`. - fn focusPaneLine(p: *Pardes, id: usize, line: usize, col: usize) void { + pub fn focusPaneLine(p: *Pardes, id: usize, line: usize, col: usize) void { if (id >= MAX_PANES) return; const pane = p.panes[id] orelse return; p.active = id; @@ -4956,7 +5008,7 @@ pub const Pardes = struct { // The word is resolved against the pane DIRECTORIES in access // order: the pane the click came from FIRST — its answer is the // one taken, so nothing that resolves today moves — then every - // other live pane, most recently focused first (focus_hist runs + // other live pane, most recently focused first (the jump stack runs // least-recent -> active, so it is that array backwards; the // clicked pane is not always `active`, a right click does not // focus). Only when ALL of them fail does the word fall through to @@ -4970,8 +5022,8 @@ pub const Pardes = struct { var seen: [MAX_PANES][]const u8 = undefined; var nseen: usize = 0; var n: usize = 0; - cand: while (n <= p.nfocus) : (n += 1) { - const k = if (n == 0) id else p.focus_hist[p.nfocus - n]; + cand: while (n <= p.njumps) : (n += 1) { + const k: usize = if (n == 0) id else p.jumps[p.njumps - n].pane; if (n > 0 and k == id) continue; // tried first, skip the rerun const op = p.panes[k] orelse continue; const dir = if (op.file) |f| (std.fs.path.dirname(f.path) orelse "/") else op.cwdSlice(); @@ -5409,11 +5461,13 @@ pub const Pardes = struct { /// where focus falls when the active pane closes: the most recently /// focused pane still alive (else any live one). Null = nothing left. + /// Walks the jump stack newest-first, so it answers exactly what it always + /// did — a pane's newest entry sits where the old MRU put the pane. pub fn prevFocus(p: *Pardes, closing: usize) ?usize { - var i = p.nfocus; + var i = p.njumps; while (i > 0) { i -= 1; - const id = p.focus_hist[i]; + const id = p.jumps[i].pane; if (id != closing and p.panes[id] != null) return id; } for (p.panes, 0..) |slot, k| { @@ -5422,26 +5476,91 @@ pub const Pardes = struct { return null; } + /// THE PUSH RULE, and the only place it is written down. + /// + /// A location is worth remembering when you cannot see it any more: focus + /// ended this update in a DIFFERENT pane, or more than a bodyful of rows + /// away in the same one. Anything closer is the cursor strolling, and the + /// current entry just follows it — so h/j/k/w/b never grow the list, while + /// a goto-line, a search hit, a goto-definition and every focus change do. + /// (Vim's rule is a hand-kept list of "jump commands"; this one asks the + /// question those commands are a proxy for, and needs no list.) + /// + /// It is read HERE, once per update, and nowhere else: what a call site + /// does transiently is invisible, which is what keeps n/N over a results + /// buffer — which focuses each hit and comes straight back — from pushing + /// two entries per keystroke. That transparency is the whole reason the + /// rule lives in sync() rather than at the sites that move focus, which is + /// where the heuristics used to be scattered. + fn trackJump(p: *Pardes) void { + // dead entries first, in one compacting pass. A slot is reused, so the + // test is the SERIAL: an entry whose pane has been replaced names a + // pane that no longer exists, not the newcomer sitting in its slot. + var w: usize = 0; + var cur: usize = 0; + for (p.jumps[0..p.njumps], 0..) |j, i| { + if (i == p.jcur) cur = w; // survives -> lands at w; dies -> the next one does + const pane = p.panes[j.pane] orelse continue; + if (pane.serial != j.serial) continue; + p.jumps[w] = j; + w += 1; + } + p.njumps = w; + p.jcur = @min(cur, w -| 1); + + const pane = p.panes[p.active] orelse return; + // An UNPINNED cursor belongs to the program on the other end of the + // pty, not to you, so such a pane is remembered as a place and not as + // a spot: line 0 is the "no line" focusPaneLine already understands, + // and going back there focuses the shell without dragging its view up + // to scrollback row 0. It also has no line to be FAR from, which is + // what keeps the first keypress in a shell (which pins the cursor + // wherever the prompt is) from reading as a jump. + const now: Loc = .{ + .pane = @intCast(p.active), + .serial = pane.serial, + .line = if (pane.cur_pinned) @intCast(@max(0, pane.cur_row) + 1) else 0, + .col = if (pane.cur_pinned) @intCast(@max(0, pane.cur_col) + 1) else 0, + }; + if (p.njumps > 0) { + const at = &p.jumps[p.jcur]; + const body = @max(1, p.rects[p.active].h -| BOX_H); + const far = at.line != 0 and now.line != 0 and + @max(now.line, at.line) - @min(now.line, at.line) > body; + if (at.pane == now.pane and !far) { + at.* = now; // same visit; the entry IS where you are + return; + } + } + // a new jump made from the middle of the list drops everything ahead of + // it, the way vim's does: the future you did not take is not history. + if (p.njumps > 0) p.njumps = p.jcur + 1; + if (p.njumps == MAX_JUMPS) { + std.mem.copyForwards(Loc, p.jumps[0 .. MAX_JUMPS - 1], p.jumps[1..]); + p.njumps -= 1; + } + p.jumps[p.njumps] = now; + p.jcur = p.njumps; + p.njumps += 1; + } + + /// Ctrl-o / Ctrl-i (the Back and Forward builtins): move the CURSOR into + /// the stack and go to what it names. Nothing is pushed and nothing is + /// dropped — walking history is not making it — and trackJump agrees, + /// because after the move the live spot IS `jumps[jcur]` again. + pub fn jumpBy(p: *Pardes, delta: i32) void { + const next = @as(i64, @intCast(p.jcur)) + delta; + if (p.njumps == 0 or next < 0 or next >= p.njumps) return; + p.jcur = @intCast(next); + const j = p.jumps[p.jcur]; + p.focusPaneLine(j.pane, j.line, j.col); + } + /// Recompute geometry, push grid-size changes to each emulator + pty, fire /// deferred greetings. The mirror of the prototype's loop epilogue. fn sync(p: *Pardes) void { p.computeGeom(); - // focus history, rebuilt in one pass: drop dead slots and the current - // active, then push active back on top. Every focus change (mouse, - // look, Ctrl-w, Alt-n) lands here without touching its call site. - { - var w: usize = 0; - for (p.focus_hist[0..p.nfocus]) |id| { - if (p.panes[id] == null or id == p.active) continue; - p.focus_hist[w] = id; - w += 1; - } - p.nfocus = w; - if (p.panes[p.active] != null) { - p.focus_hist[p.nfocus] = p.active; - p.nfocus += 1; - } - } + p.trackJump(); for (&p.panes, 0..) |*slot, id| { const pane = slot.* orelse continue; if (pane.reply_len > 0) { diff --git a/test/snapshots/jumps.golden b/test/snapshots/jumps.golden new file mode 100644 index 00000000..c49b354e --- /dev/null +++ b/test/snapshots/jumps.golden @@ -0,0 +1,527 @@ +== snap start grid=150x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| 22 line 22 +| 23 line 23 +| 24 line 24 +| 25 line 25 +| 26 line 26 +| 27 line 27 +| 28 line 28 +== snap to-shell grid=150x30 cursor=79,4 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| 22 line 22 +| 23 line 23 +| 24 line 24 +| 25 line 25 +| 26 line 26 +| 27 line 27 +| 28 line 28 +== snap to-file grid=150x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| 22 line 22 +| 23 line 23 +| 24 line 24 +| 25 line 25 +| 26 line 26 +| 27 line 27 +| 28 line 28 +== snap goto-100 grid=150x30 cursor=7,26 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 76 line 76 +| 77 line 77 big.txt +| 78 line 78 +| 79 line 79 +| 80 line 80 +| 81 line 81 +| 82 line 82 +| 83 line 83 +| 84 line 84 +| 85 line 85 +| 86 line 86 +| 87 line 87 +| 88 line 88 +| 89 line 89 +| 90 line 90 +| 91 line 91 +| 92 line 92 +| 93 line 93 +| 94 line 94 +| 95 line 95 +| 96 line 96 +| 97 line 97 +| 98 line 98 +| 99 line 99 +| 100 line 100 +| 101 line 101 +| 102 line 102 +| 103 line 103 +== snap back-in-file grid=150x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| 22 line 22 +| 23 line 23 +| 24 line 24 +| 25 line 25 +| 26 line 26 +| 27 line 27 +| 28 line 28 +== snap back-to-shell grid=150x30 cursor=79,4 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| 22 line 22 +| 23 line 23 +| 24 line 24 +| 25 line 25 +| 26 line 26 +| 27 line 27 +| 28 line 28 +== snap forward-file grid=150x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| 22 line 22 +| 23 line 23 +| 24 line 24 +| 25 line 25 +| 26 line 26 +| 27 line 27 +| 28 line 28 +== snap forward-100 grid=150x30 cursor=7,16 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 86 line 86 +| 87 line 87 big.txt +| 88 line 88 +| 89 line 89 +| 90 line 90 +| 91 line 91 +| 92 line 92 +| 93 line 93 +| 94 line 94 +| 95 line 95 +| 96 line 96 +| 97 line 97 +| 98 line 98 +| 99 line 99 +| 100 line 100 +| 101 line 101 +| 102 line 102 +| 103 line 103 +| 104 line 104 +| 105 line 105 +| 106 line 106 +| 107 line 107 +| 108 line 108 +| 109 line 109 +| 110 line 110 +| 111 line 111 +| 112 line 112 +| 113 line 113 +== snap last grid=150x30 cursor=79,4 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 86 line 86 +| 87 line 87 big.txt +| 88 line 88 +| 89 line 89 +| 90 line 90 +| 91 line 91 +| 92 line 92 +| 93 line 93 +| 94 line 94 +| 95 line 95 +| 96 line 96 +| 97 line 97 +| 98 line 98 +| 99 line 99 +| 100 line 100 +| 101 line 101 +| 102 line 102 +| 103 line 103 +| 104 line 104 +| 105 line 105 +| 106 line 106 +| 107 line 107 +| 108 line 108 +| 109 line 109 +| 110 line 110 +| 111 line 111 +| 112 line 112 +| 113 line 113 +== snap last-back grid=150x30 cursor=7,16 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 86 line 86 +| 87 line 87 big.txt +| 88 line 88 +| 89 line 89 +| 90 line 90 +| 91 line 91 +| 92 line 92 +| 93 line 93 +| 94 line 94 +| 95 line 95 +| 96 line 96 +| 97 line 97 +| 98 line 98 +| 99 line 99 +| 100 line 100 +| 101 line 101 +| 102 line 102 +| 103 line 103 +| 104 line 104 +| 105 line 105 +| 106 line 106 +| 107 line 107 +| 108 line 108 +| 109 line 109 +| 110 line 110 +| 111 line 111 +| 112 line 112 +| 113 line 113 +== snap jumplist grid=150x30 cursor=7,16 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 86 line 86 +| 87 line 87 big.txt +| 88 line 88 +| 89 line 89 +| 90 line 90 +| 91 line 91 +| 92 line 92 +| 93 line 93 +| 94 line 94 +| 95 line 95 +| 96 line 96 +| 97 line 97 +| 98 line 98 +| 99 line 99 +| 100 line 100 +| 101 line 101 +| 102 line 102 +| 103 line 103 +| 104 line 104 +| 105 line 105 +| 106 line 106 +| NOR /tmp/pardes-snap/jumps/cwd/+Jumps Del +| 1 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 2 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 3 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +| 5 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 6 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +== snap step1 grid=150x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| NOR /tmp/pardes-snap/jumps/cwd/+Jumps Del +| 1 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 2 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 3 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +| 5 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 6 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +== style step1 +|0: 0-52 #949494,#121212, 53-149 d,#121212, +|1: 0-1 d,#626262, 2-48 #949494,#121212, 49-74 d,#121212, 75-76 d,#262626, 77-110 #949494,#121212, 111-149 d,#121212, +|2: 0-1 d,#444444, 2-6 #949494,#121212, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|3: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|4: 0-1 d,#121212, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|5: 0-1 d,#121212, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|6: 0-1 d,#121212, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|7: 0-1 d,#121212, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|8: 0-1 d,#121212, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|9: 0-1 d,#121212, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|10: 0-1 d,#121212, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|11: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|12: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|13: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|14: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|15: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|16: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|17: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|18: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|19: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|20: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|21: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|22: 0-1 d,#121212, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|23: 0-1 d,#262626, 2-42 #949494,#121212, 43-74 d,#121212, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|24: 0-1 d,#444444, 2-6 #626262,#080808, 7-74 #d8c8a8,#4a3a1e, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|25: 0-1 d,#444444, 2-6 #626262,#080808, 7-40 #bdbdbd,#080808, 41-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|26: 0-1 d,#444444, 2-6 #626262,#080808, 7-51 #bdbdbd,#080808, 52-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|27: 0-1 d,#444444, 2-6 #626262,#080808, 7-55 #bdbdbd,#080808, 56-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|28: 0-1 d,#444444, 2-6 #626262,#080808, 7-40 #bdbdbd,#080808, 41-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +|29: 0-1 d,#121212, 2-6 #626262,#080808, 7-55 #bdbdbd,#080808, 56-74 d,#080808, 75-76 d,#444444, 77-149 #bdbdbd,#080808, +== snap step2-shell grid=150x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| NOR /tmp/pardes-snap/jumps/cwd/+Jumps Del +| 1 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 2 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 3 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +| 5 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 6 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +== snap step4-line100 grid=150x30 cursor=7,12 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 90 line 90 +| 91 line 91 big.txt +| 92 line 92 +| 93 line 93 +| 94 line 94 +| 95 line 95 +| 96 line 96 +| 97 line 97 +| 98 line 98 +| 99 line 99 +| 100 line 100 +| 101 line 101 +| 102 line 102 +| 103 line 103 +| 104 line 104 +| 105 line 105 +| 106 line 106 +| 107 line 107 +| 108 line 108 +| 109 line 109 +| 110 line 110 +| NOR /tmp/pardes-snap/jumps/cwd/+Jumps Del +| 1 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 2 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 3 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +| 5 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 6 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +== snap step-back grid=150x30 cursor=7,2 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 1 line 1 +| 2 line 2 big.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 line 6 +| 7 line 7 +| 8 line 8 +| 9 line 9 +| 10 line 10 +| 11 line 11 +| 12 line 12 +| 13 line 13 +| 14 line 14 +| 15 line 15 +| 16 line 16 +| 17 line 17 +| 18 line 18 +| 19 line 19 +| 20 line 20 +| 21 line 21 +| NOR /tmp/pardes-snap/jumps/cwd/+Jumps Del +| 1 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 2 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 3 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +| 5 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 6 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +== snap look-row grid=150x30 cursor=7,12 +|Kill Newcol Tutor Debug NextColor Dump Find Grep Help +| NOR /tmp/pardes-snap/jumps/cwd/big.txt Save Del NOR /tmp/pardes-snap/jumps/cwd Del +| 90 line 90 +| 91 line 91 big.txt +| 92 line 92 +| 93 line 93 +| 94 line 94 +| 95 line 95 +| 96 line 96 +| 97 line 97 +| 98 line 98 +| 99 line 99 +| 100 line 100 +| 101 line 101 +| 102 line 102 +| 103 line 103 +| 104 line 104 +| 105 line 105 +| 106 line 106 +| 107 line 107 +| 108 line 108 +| 109 line 109 +| 110 line 110 +| NOR /tmp/pardes-snap/jumps/cwd/+Jumps Del +| 1 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 2 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 3 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1 +| 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 +| 5 @p1:3:3 /tmp/pardes-snap/jumps/cwd +| 6 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100 diff --git a/test/snapshots/jumps.snap b/test/snapshots/jumps.snap new file mode 100644 index 00000000..900b63a1 --- /dev/null +++ b/test/snapshots/jumps.snap @@ -0,0 +1,85 @@ +# RULE: there is ONE focus history and it is a stack of LOCATIONS — a pane and +# a spot in it. Ctrl-o walks it back, Ctrl-i walks it forward again, `SPC j j` +# (Last) hops to the newest OTHER pane, and `SPC j l` (Jumplist) writes the +# same stack out as an output buffer whose rows are ordinary look targets, so +# n/N step them. What gets ONTO the stack is decided in one place (trackJump): +# focus ended the update in a different pane, or more than a bodyful of rows +# away in the same one — which is why `100G` below leaves a location behind +# and the j/k that follow it do not. +# +# Focus and position both show in the golden's `cursor=` header: that is the +# ACTIVE pane's cursor, so each snap is named by where it should land. +lines big.txt 200 +start 30 150 big.txt +wait 8000 big.txt +stable 700 20000 +# left column = big.txt, right column = the launch shell; the file has focus +snap start +# two focus moves, so the stack is file -> shell -> file +key space w l +stable 400 5000 +snap to-shell +key space w h +stable 400 5000 +snap to-file +# a FAR motion inside one pane: 100G is a jump, so the line we left is +# remembered and Ctrl-o can come back to it +text 100 +key G +stable 400 5000 +snap goto-100 +# Ctrl-o: back to line 1 of the same pane — a stack of panes could not do this +key c-o +stable 400 5000 +snap back-in-file +# ...and back again into the shell +key c-o +stable 400 5000 +snap back-to-shell +# Ctrl-i forward, twice: the shell entry, then line 100 again. Sent as the +# kitty-protocol CSI u (`CSI 105;5u`) because on the wire Ctrl-i IS Tab — +# see config.jump_keys. +send \e[105;5u +stable 400 5000 +snap forward-file +send \e[105;5u +stable 400 5000 +snap forward-100 +# SPC j j — the newest OTHER pane, and pressing it again comes straight back +key space j j +stable 400 5000 +snap last +key space j j +stable 400 5000 +snap last-back +# SPC j l — the stack as text: `location text` rows, the file naming itself +# and the shell spelled @pN because it has no path +key space j l +stable 700 15000 +snap jumplist +# n/N step the rows and look them, exactly as they do a +Search: the stepped +# row is selected in the buffer (snapstyle) and the pane it names follows. +# Row 1 is the file at line 1, row 2 the shell (no path, so `@pN`), row 4 the +# file at line 100 — so the body visibly rides the list. +key n +stable 700 15000 +snap step1 +snapstyle step1 +key n +stable 700 15000 +snap step2-shell +key n +stable 700 15000 +key n +stable 700 15000 +snap step4-line100 +key N +stable 700 15000 +snap step-back +# and an ordinary LOOK on a row goes there too — the leading word IS a +# location, so the right-click look resolves it like any other path. Row 4 +# again, this time by clicking it. +press right 16 28 +release right 16 28 +stable 700 15000 +snap look-row diff --git a/test/snapshots/leader.golden b/test/snapshots/leader.golden index 6f88ad7e..831d4557 100644 --- a/test/snapshots/leader.golden +++ b/test/snapshots/leader.golden @@ -196,13 +196,13 @@ | 9 SPC f g Grep | 10 SPC f s Save | 11 SPC h t Tutor -| 12 SPC k Kill -| 13 SPC l D WsDiagnostics -| 14 SPC l S WsSymbols -| 15 SPC l a CodeAction -| 16 SPC l d Diagnostics -| 17 SPC l h SelectRefs -| 18 SPC l i Lspinfo +| 12 SPC j i Forward +| 13 SPC j j Last +| 14 SPC j l Jumplist +| 15 SPC j o Back +| 16 SPC k Kill +| 17 SPC l D WsDiagnostics +| 18 SPC l S WsSymbols == snap help-group grid=100x40 cursor=7,22 |Kill Newcol Tutor Debug NextColor Dump Find Grep Help | NOR /tmp/pardes-snap/leader/cwd/cmds.txt Save De NOR /tmp/pardes-snap/leader/cwd Del @@ -360,13 +360,13 @@ | 9 SPC f g Grep | 10 SPC f s Save | 11 SPC h t Tutor -| 12 SPC k Kill -| 13 SPC l D WsDiagnostics -| 14 SPC l S WsSymbols -| 15 SPC l a CodeAction -| 16 SPC l d Diagnostics -| 17 SPC l h SelectRefs -| 18 SPC l i Lspinfo +| 12 SPC j i Forward +| 13 SPC j j Last +| 14 SPC j l Jumplist +| 15 SPC j o Back +| 16 SPC k Kill +| 17 SPC l D WsDiagnostics +| 18 SPC l S WsSymbols == snap help-toggles grid=100x40 cursor=7,22 |Kill Newcol Tutor Debug NextColor Dump Find Grep Help | NOR /tmp/pardes-snap/leader/cwd/cmds.txt Save De NOR /tmp/pardes-snap/leader/cwd Del diff --git a/test/snapshots/ttyhelp.golden b/test/snapshots/ttyhelp.golden index 6cb6d930..ec3a4fd6 100644 --- a/test/snapshots/ttyhelp.golden +++ b/test/snapshots/ttyhelp.golden @@ -45,21 +45,21 @@ | 9 SPC f g Grep | 10 SPC f s Save | 11 SPC h t Tutor -| 12 SPC k Kill -| 13 SPC l D WsDiagnostics -| 14 SPC l S WsSymbols -| 15 SPC l a CodeAction -| 16 SPC l d Diagnostics -| 17 SPC l h SelectRefs -| 18 SPC l i Lspinfo -| 19 SPC l k Hover -| 20 SPC l r Rename -| 21 SPC l s Symbols -| 22 SPC l w Lspwhy -| 23 SPC s d Dump -| 24 SPC s r Restore -| 25 SPC t a Ascii -| 26 SPC t c Colors +| 12 SPC j i Forward +| 13 SPC j j Last +| 14 SPC j l Jumplist +| 15 SPC j o Back +| 16 SPC k Kill +| 17 SPC l D WsDiagnostics +| 18 SPC l S WsSymbols +| 19 SPC l a CodeAction +| 20 SPC l d Diagnostics +| 21 SPC l h SelectRefs +| 22 SPC l i Lspinfo +| 23 SPC l k Hover +| 24 SPC l r Rename +| 25 SPC l s Symbols +| 26 SPC l w Lspwhy == snap leaderworks grid=100x30 cursor=7,4 |Kill Newcol Tutor Debug NextColor Dump Find Grep Help | TTY /tmp/pardes-snap/ttyhelp/cwd Del @@ -76,18 +76,18 @@ | 9 SPC f g Grep | 10 SPC f s Save | 11 SPC h t Tutor -| 12 SPC k Kill -| 13 SPC l D WsDiagnostics -| 14 SPC l S WsSymbols -| 15 SPC l a CodeAction -| 16 SPC l d Diagnostics -| 17 SPC l h SelectRefs -| 18 SPC l i Lspinfo -| 19 SPC l k Hover -| 20 SPC l r Rename -| 21 SPC l s Symbols -| 22 SPC l w Lspwhy -| 23 SPC s d Dump -| 24 SPC s r Restore -| 25 SPC t a Ascii -| 26 SPC t c Colors +| 12 SPC j i Forward +| 13 SPC j j Last +| 14 SPC j l Jumplist +| 15 SPC j o Back +| 16 SPC k Kill +| 17 SPC l D WsDiagnostics +| 18 SPC l S WsSymbols +| 19 SPC l a CodeAction +| 20 SPC l d Diagnostics +| 21 SPC l h SelectRefs +| 22 SPC l i Lspinfo +| 23 SPC l k Hover +| 24 SPC l r Rename +| 25 SPC l s Symbols +| 26 SPC l w Lspwhy -- cgit v1.3