diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 17:52:24 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 18:02:10 -0300 |
| commit | afec162464d360cbbf475d3fb7457cda83ce918f (patch) | |
| tree | 7278bc32743769c096fd743758ea3a2967b8be93 | |
| parent | fe9ec319e01052e3ede65ad0e8a809dd5f2c9953 (diff) | |
| download | pardes-afec162464d360cbbf475d3fb7457cda83ce918f.tar.gz pardes-afec162464d360cbbf475d3fb7457cda83ce918f.zip | |
Back and Forward keep to the focused file: Ctrl-o, Ctrl-i, the mouse's side buttons (now the tty's 8 and 9 too), the builtins and 9P go to the nearest place that way in this pane, else in this file in another pane, else of any file; JumpScope all takes every place in order as before
| -rw-r--r-- | docs/typ/builtins-gui.json | 1 | ||||
| -rw-r--r-- | docs/typ/builtins-tty.json | 1 | ||||
| -rw-r--r-- | docs/typ/cheatsheet.typ | 2 | ||||
| -rw-r--r-- | docs/typ/guide.typ | 2 | ||||
| -rw-r--r-- | docs/typ/reference.typ | 1 | ||||
| -rw-r--r-- | src/config.zig | 11 | ||||
| -rw-r--r-- | src/dump.zig | 3 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 47 | ||||
| -rw-r--r-- | src/pardes.zig | 203 | ||||
| -rw-r--r-- | src/tty/tty.zig | 10 |
10 files changed, 272 insertions, 9 deletions
diff --git a/docs/typ/builtins-gui.json b/docs/typ/builtins-gui.json index e4a8f611..53142c1e 100644 --- a/docs/typ/builtins-gui.json +++ b/docs/typ/builtins-gui.json @@ -50,6 +50,7 @@ {"name": "HoverGlow", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "on, off", "doc": "A soft underline of light under the word a look-hover would open, on or off."}, {"name": "InactiveDim", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": null, "doc": "Fade the unfocused panes' text by a percentage, 0 through 100, 0 off."}, {"name": "Joincol", "leader": "SPC c j", "also": ["topbar"], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "Fold the active pane's column into the one on its right, keeping its panes."}, + {"name": "JumpScope", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "file, all", "doc": "What Back and Forward keep to: file, the focused pane's file until it runs out that way, or all, every place in order."}, {"name": "JumpTrail", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "on, off", "doc": "In a terminal, a jump of the cursor leaves a short trail that fades, on or off."}, {"name": "Jumplist", "leader": "SPC j l", "also": [], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "List the places jumped from, each a place to go."}, {"name": "Kill", "leader": null, "also": [], "arg": true, "scope": "root", "setting": false, "choices": null, "doc": "acme's Kill: stop the commands pardes started -- the lines it typed into a terminal, from an exec, a middle click or a pty/run, while their shell says they run (C mark to D)."}, diff --git a/docs/typ/builtins-tty.json b/docs/typ/builtins-tty.json index 959f88ce..d3dac97c 100644 --- a/docs/typ/builtins-tty.json +++ b/docs/typ/builtins-tty.json @@ -42,6 +42,7 @@ {"name": "Hover", "leader": "SPC l k", "also": [], "arg": false, "scope": "pane", "setting": false, "choices": null, "doc": "Ask the language server about the symbol under the cursor."}, {"name": "InactiveDim", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": null, "doc": "Fade the unfocused panes' text by a percentage, 0 through 100, 0 off."}, {"name": "Joincol", "leader": "SPC c j", "also": ["topbar"], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "Fold the active pane's column into the one on its right, keeping its panes."}, + {"name": "JumpScope", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "file, all", "doc": "What Back and Forward keep to: file, the focused pane's file until it runs out that way, or all, every place in order."}, {"name": "JumpTrail", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "on, off", "doc": "In a terminal, a jump of the cursor leaves a short trail that fades, on or off."}, {"name": "Jumplist", "leader": "SPC j l", "also": [], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "List the places jumped from, each a place to go."}, {"name": "Kill", "leader": null, "also": [], "arg": true, "scope": "root", "setting": false, "choices": null, "doc": "acme's Kill: stop the commands pardes started -- the lines it typed into a terminal, from an exec, a middle click or a pty/run, while their shell says they run (C mark to D)."}, diff --git a/docs/typ/cheatsheet.typ b/docs/typ/cheatsheet.typ index e95f5076..d2df7e70 100644 --- a/docs/typ/cheatsheet.typ +++ b/docs/typ/cheatsheet.typ @@ -74,7 +74,7 @@ Helix-style: motions *select*, then an edit acts on the selection. Mode box: bla [#key("Ctrl-w") #keys("h", "j", "k", "l")], [then a direction: focus a neighbour (not in insert or raw `$`)], [#key("Esc")], [back to the previous pane (raw: only at a prompt with nothing typed; insert: to normal)], [#key("Shift-Esc")], [back to the previous pane from any mode, with the kitty keyboard protocol or in the GUI; elsewhere a plain Esc], - [#keys("Ctrl-o", "Ctrl-i")], [jump history back, forward], + [#keys("Ctrl-o", "Ctrl-i")], [jump history back, forward; this file first], [#key("Alt-n")], [new terminal below], [#key("Alt-c")], [move the pane to a new column], ) diff --git a/docs/typ/guide.typ b/docs/typ/guide.typ index 5bfff0ce..90e72303 100644 --- a/docs/typ/guide.typ +++ b/docs/typ/guide.typ @@ -55,7 +55,7 @@ the next pane down the column. #pairs( [#key("Ctrl-w") #key("h")], [the keyboard to the pane on the left; #key("j") below, #key("k") above, #key("l") right], - [#keys("Ctrl-o", "Ctrl-i")], [back and forward through the places you jumped to (the jump list)], + [#keys("Ctrl-o", "Ctrl-i")], [back and forward through the places you jumped to (the jump list), this file's first: another file's only once it has none left that way (#word("JumpScope") `all`: every place in order)], ) Raw mode sends almost every key to the program. Leave it (#key("Ctrl-b")) diff --git a/docs/typ/reference.typ b/docs/typ/reference.typ index a7ac997e..bcdf40e2 100644 --- a/docs/typ/reference.typ +++ b/docs/typ/reference.typ @@ -404,6 +404,7 @@ word clicked in a tag does. The same lines go in the startup file [#word("BootShell") `keep|replace`], [`keep`; `replace` closes the untouched lone shell a dragged document lands beside], [#word("LookWord") `search|list`], [`search`: a word given to #word("Look") selects its next place, or lists all in `+Search`], [#word("DirLook") `pane|terminal`], [`pane`: #word("Look") on a directory opens a pane listing it, as acme's directory window (#doc("reference", section: "from-acme")); `terminal` types `ls` into a terminal idle there, else opens one], + [#word("JumpScope") `file|all`], [`file`: #word("Back") and #word("Forward") keep to the focused pane's file, that pane's places first, then the file's in another pane, and go to another file only when the file has no place left that way; from a terminal or a pane with no file they step through every place; `all`: every place, in the order it was jumped to], [#word("TermImages") `real|petscii`], [`real`: a new terminal draws its program's kitty graphics (yazi's previews) as pixels where the shell can; #word("Petscii") flips one terminal; a tty under a terminal without kitty graphics draws them as glyph art either way], [#word("Shell") `<name or path>`], [`$SHELL` when it is executable, else `/bin/sh`: the shell the next terminal and command pane run; a bare name is searched for in the usual bin directories, not `$PATH`; bare #word("Shell"): `$SHELL` if it is executable, else `/bin/sh`], [#word("DumpDir") `<dir>`], [`$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`: where #word("Dump") writes, an absolute or `~` path to a directory that may be written (made if missing); a relative one, or one under a directory that may not be written, is refused; bare returns to the default], diff --git a/src/config.zig b/src/config.zig index 45deb7bb..d80e2a02 100644 --- a/src/config.zig +++ b/src/config.zig @@ -85,6 +85,7 @@ pub const leader_path = paths: { .Pager = null, .PagerColor = null, .TermImages = null, + .JumpScope = null, .Debug = "td", .Msg = null, .Colors = "tc", @@ -791,6 +792,10 @@ pub const Runtime = struct { /// (yazi's previews): `real`, as pixels where the shell can, or /// `petscii`, as glyph art. The Petscii builtin flips one pane. term_images: TermImages = .real, + /// What Back and Forward keep to (Pardes.jumpBy): `file`, the places in + /// the focused pane's file first, other files only once those run out + /// that way; `all`, every place in the order it was jumped to. + jump_scope: JumpScope = .file, pub const ShaderAnimation = enum { off, on, always }; pub const Placement = enum { acme, pardes }; @@ -799,8 +804,9 @@ pub const Runtime = struct { pub const DirLook = enum { pane, terminal }; pub const Pager = enum { pardes, off }; pub const TermImages = enum { real, petscii }; + pub const JumpScope = enum { file, all }; /// A setting that is one of a few words, each an enum field of Runtime. - pub const Choice = enum { placement, boot_shell, look_word, dir_look, pager, term_images }; + pub const Choice = enum { placement, boot_shell, look_word, dir_look, pager, term_images, jump_scope }; /// shadow: a soft drop shadow. rim: a light hairline along the top of /// the focused pane's tag rows. auto: a shadow on a light page; on a @@ -1069,6 +1075,7 @@ pub const Runtime = struct { .{ .word = "Pager", .action = .{ .choice = .pager }, .doc = "What a terminal's commands page through: pardes, a +Pager pane (pardes -), or off, the environment's own; for terminals started after it." }, .{ .word = "PagerColor", .action = .{ .toggle = .pager_color }, .doc = "A +Pager keeps its program's colours, on or off; for the next text paged." }, .{ .word = "TermImages", .action = .{ .choice = .term_images }, .doc = "How a new terminal draws its program's kitty graphics: real, as pixels where it can, or petscii, as glyph art." }, + .{ .word = "JumpScope", .action = .{ .choice = .jump_scope }, .doc = "What Back and Forward keep to: file, the focused pane's file until it runs out that way, or all, every place in order." }, }; pub fn find(name: []const u8) ?Setting { @@ -1756,6 +1763,7 @@ pub const Runtime = struct { .dir_look => "terminal", .pager => "off", .term_images => "petscii", + .jump_scope => "all", }, .lift => "rim", .motion => "playful", @@ -1829,6 +1837,7 @@ pub const Runtime = struct { .dir_look => "DirLook terminal\n", .pager => "Pager off\n", .term_images => "TermImages petscii\n", + .jump_scope => "JumpScope all\n", }, .lift => "Lift rim\n", .motion => "Motion playful\n", diff --git a/src/dump.zig b/src/dump.zig index 84321457..005770e1 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -1028,14 +1028,17 @@ test "a dump keeps the settings that differ from a fresh session's, and a restor defer p.deinit(); try std.testing.expect(p.executeBuiltinLine(p.active, "Placement pardes")); try std.testing.expect(p.executeBuiltinLine(p.active, "Verbose off")); + try std.testing.expect(p.executeBuiltinLine(p.active, "JumpScope all")); try dumpState(p); try std.testing.expect(std.mem.indexOf(u8, p.dump_out.?, "\"Placement pardes\"") != null); + try std.testing.expect(std.mem.indexOf(u8, p.dump_out.?, "\"JumpScope all\"") != null); // What is left at its default is not written. try std.testing.expect(std.mem.indexOf(u8, p.dump_out.?, "\"BootShell keep\"") == null); const restored = try initFromDump(gpa, .{ .tty_only = true }, p.dump_out.?); defer restored.deinit(); try std.testing.expectEqual(config.Runtime.Placement.pardes, restored.settings.placement); try std.testing.expect(!restored.settings.verbose); + try std.testing.expectEqual(config.Runtime.JumpScope.all, restored.settings.jump_scope); } test "a session with every column it may hold dumps and restores them all" { diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 157bc8b9..0f4e181c 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -3576,6 +3576,53 @@ test "Back and Forward over a closed file's entries: one reopen is that file's p try testing.expectEqual(c_serial, p.panes[p.active].?.serial); } +test "Back and Forward written to exec keep to the focused file, and JumpScope all, written to ctl, takes every place in order" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "a.txt", .data = "row\n" ** 100 }); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "b.txt", .data = "row\n" ** 100 }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + var line: [4200]u8 = undefined; + // Each a look, as a user's jumps are: b1 a1 b40 a40 b80 a80. + for ([_][]const u8{ "b.txt:1", "a.txt:1", "b.txt:40", "a.txt:40", "b.txt:80", "a.txt:80" }) |place| { + try testing.expectEqual(Status.ok, wr(p, root_look, try std.fmt.bufPrint(&line, "{s}/{s}\n", .{ dir, place })).reply.status); + p.sync(); + } + const Step = struct { word: []const u8, file: []const u8, line: i32 }; + const walk = struct { + fn run(q: *Pardes, steps: []const Step) !void { + for (steps) |s| { + var cmd: [16]u8 = undefined; + try testing.expectEqual(Status.ok, wr(q, root_exec, try std.fmt.bufPrint(&cmd, "{s}\n", .{s.word})).reply.status); + q.sync(); + const pane = q.panes[q.active].?; + try testing.expect(std.mem.endsWith(u8, pane.file.?.path, s.file)); + try testing.expectEqual(s.line, pane.body.cur_row + 1); + } + } + }.run; + // a's places first, b only once a has none older; Forward mirrors it + // from b and ends at a80, where it began. + try walk(p, &.{ + .{ .word = "Back", .file = "/a.txt", .line = 40 }, + .{ .word = "Back", .file = "/a.txt", .line = 1 }, + .{ .word = "Back", .file = "/b.txt", .line = 1 }, + .{ .word = "Forward", .file = "/b.txt", .line = 40 }, + .{ .word = "Forward", .file = "/b.txt", .line = 80 }, + .{ .word = "Forward", .file = "/a.txt", .line = 80 }, + }); + try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "JumpScope all\n").reply.status); + try testing.expect(std.mem.indexOf(u8, rd(p, @intFromEnum(tree.TopFile.ctl), 0, 8192).bytes, "JumpScope all\n") != null); + try walk(p, &.{ + .{ .word = "Back", .file = "/b.txt", .line = 80 }, + .{ .word = "Back", .file = "/a.txt", .line = 40 }, + .{ .word = "Forward", .file = "/b.txt", .line = 80 }, + }); +} + test "Recent keeps only files on disk: not a name never saved, nor the served tree's" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); diff --git a/src/pardes.zig b/src/pardes.zig index 164bc253..674128a1 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -7073,12 +7073,42 @@ pub const Pardes = struct { p.njumps += 1; } + /// One step through the jumplist, older for a negative `delta`, newer + /// otherwise: Back and Forward from every key, mouse button, builtin and + /// 9P exec. `jcur` is the entry where you are; the step moves it to the + /// entry it lands on, and the entries it passed over stay where they are. + /// + /// JumpScope file, from a pane with a file: the nearest entry that way in + /// this pane, else in this file in any pane (a closed one's too), else, + /// with none of this file left that way, the nearest of any file, as + /// JumpScope all always takes. Forward mirrors Back, so it retraces a run + /// of Backs inside one file step for step, and Forwards end, as they + /// always have, at the newest entry. pub fn jumpBy(p: *Pardes, delta: i32) void { - const next = @as(i64, @intCast(p.jcur)) + delta; + const older = delta < 0; // At an end of the list: said, not a step that silently does nothing. - if (p.njumps == 0 or next < 0 or next >= p.njumps) - return p.setMessage(p.active, if (delta < 0) "Back: nothing further back" else "Forward: nothing further on"); - const j = p.jumps[@intCast(next)]; + if (p.njumps == 0 or (older and p.jcur == 0) or (!older and p.jcur + 1 >= p.njumps)) + return p.setMessage(p.active, if (older) "Back: nothing further back" else "Forward: nothing further on"); + var next = if (older) p.jcur - 1 else p.jcur + 1; + // A terminal or a scratch has no file to keep to: the step is global. + if (p.settings.jump_scope == .file) if (p.panes[p.active]) |here| if (recentPlace(here)) |place| if (place.path.len > 0) { + const hash = if (recentDot(here)) |d| std.hash.Wyhash.hash(0, d.path) else 0; + // The nearest of this pane's own wins over a nearer one of this + // file in another pane: the step does not move the keyboard. + var pick: ?usize = null; + var i = p.jcur; + while (if (older) i > 0 else i + 1 < p.njumps) { + i = if (older) i - 1 else i + 1; + const k = p.jumps[i]; + if (k.pane == p.active and k.serial == here.serial) { + pick = i; + break; + } + if (pick == null and hash != 0 and k.path_hash == hash) pick = i; + } + next = pick orelse next; + }; + const j = p.jumps[next]; if (j.pane >= MAX_PANES) return; // A closed file's entry: opened again at its dot, placed as a look // places it, and the entry is that pane from here on. @@ -7117,14 +7147,14 @@ pub const Pardes = struct { k.pane = @intCast(p.active); k.serial = opened.serial; }; - p.jcur = @intCast(next); + p.jcur = next; return; } const pane = p.panes[j.pane] orelse return; if (pane.serial != j.serial) return; if (p.panes[p.active]) |source| look.clearNavigationSelection(p, source); if (p.active != j.pane) look.clearNavigationSelection(p, pane); - p.jcur = @intCast(next); + p.jcur = next; look.focusPaneLine(p, j.pane, .{ .line = j.line, .col = j.col }, .center); } @@ -8699,6 +8729,167 @@ test "jump history clears selections and lands at the recorded cursor" { } } +/// A session for the JumpScope tests: four panes of 100 lines in one +/// column, A (pane 0) and B each on a file of its own, C on A's file, and O +/// an output buffer, no file behind it. `order` is the jumplist, a letter a +/// place, each letter's n-th place its line 10 + 40n: "ABABA" is A10 B10 A50 +/// B50 A90. The keyboard is on the last place, as after jumping there. +const JumpsTest = struct { + p: *Pardes, + ids: [4]usize = .{ 0, 0, 0, 0 }, + + const letters = "ABCO"; + const Way = enum { call, key, builtin, mouse }; + + fn init(order: []const u8) !JumpsTest { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 80 }); + errdefer p.deinit(); + const text = "row\n" ** 100; + _ = try p.setTestFile(text); + var t: JumpsTest = .{ .p = p }; + for ([_][]const u8{ "/b.txt", "/test.txt", "" }, 1..) |path, n| { + const id = p.freeSlot().?; + const pane = try p.newDocPane(id); + pane.file = .{ + .path = try p.gpa.dupe(u8, path), + .content = try p.gpa.dupe(u8, text), + .history = try panes.File.History.create(p.gpa), + }; + if (path.len == 0) pane.file.?.output = .{ .from = .search }; + layout.insert(p, 0, n, id); + t.ids[n] = id; + } + layout.compute(p); + var seen: [4]u32 = @splat(0); + for (order, 0..) |letter, i| { + const which = std.mem.indexOfScalar(u8, letters, letter).?; + const pane = p.panes[t.ids[which]].?; + p.jumps[i] = .{ + .pane = @intCast(t.ids[which]), + .serial = pane.serial, + .line = 10 + 40 * seen[which], + .col = 1, + .path_hash = if (letter == 'O') 0 else std.hash.Wyhash.hash(0, pane.file.?.path), + }; + seen[which] += 1; + } + p.njumps = order.len; + p.jcur = order.len - 1; + look.focusPaneLine(p, p.jumps[p.jcur].pane, .{ .line = p.jumps[p.jcur].line, .col = 1 }, .center); + p.sync(); + return t; + } + + /// One step `delta` through `way`: the function, Ctrl-o or Ctrl-i, the + /// mouse's side button pressed and let go, or the builtin's word. + fn step(t: JumpsTest, way: Way, delta: i32) void { + const back = delta < 0; + switch (way) { + .call => t.p.jumpBy(delta), + .key => t.p.update(.{ .key = .{ .cp = if (back) 'o' else 'i', .ctrl = true } }), + .builtin => _ = t.p.executeBuiltinLine(t.p.active, if (back) "Back" else "Forward"), + .mouse => for ([_]Mouse.Kind{ .press, .release }) |kind| + t.p.update(.{ .mouse = .{ .button = if (back) .back else .forward, .kind = kind, .col = 0, .row = 0 } }), + } + t.p.sync(); + } + + /// Steps `delta` until the list ends that way, saying so; the places + /// landed on, as "A50 A10 B10". + fn walk(t: JumpsTest, way: Way, delta: i32, buf: []u8) ![]const u8 { + var out: std.Io.Writer = .fixed(buf); + for (0..2 * MAX_JUMPS) |_| { + const before = t.p.jcur; + t.step(way, delta); + if (t.p.jcur == before) break; + const pane = t.p.panes[t.p.active].?; + const which = std.mem.indexOfScalar(usize, &t.ids, t.p.active).?; + try out.print("{s}{c}{d}", .{ if (out.end > 0) " " else "", letters[which], pane.body.cur_row + 1 }); + } else return error.NoEnd; + const pane = t.p.panes[t.p.active].?; + const said = pane.msg[0..pane.msg_len]; + try std.testing.expect(std.mem.indexOf(u8, said, if (delta < 0) "nothing further back" else "nothing further on") != null); + return out.buffered(); + } +}; + +test "JumpScope file: Back keeps to the focused file, A50 then A10, and leaves it only once it runs out that way" { + const t = try JumpsTest.init("BABABA"); + defer t.p.deinit(); + try std.testing.expectEqual(config.Runtime.JumpScope.file, t.p.settings.jump_scope); + var buf: [128]u8 = undefined; + // B90, B50 passed over, kept in their places: A has nothing older than + // A10, so the step after it is the nearest place of any file, B10. + try std.testing.expectEqualStrings("A50 A10 B10", try t.walk(.call, -1, &buf)); + try std.testing.expectEqual(@as(usize, 6), t.p.njumps); + // Forward is the mirror, from B10 in B: B's own places first, then A90, + // the newest, where the Backs started. + try std.testing.expectEqualStrings("B50 B90 A90", try t.walk(.call, 1, &buf)); + try std.testing.expectEqual(@as(usize, 5), t.p.jcur); +} + +test "JumpScope file: Forward retraces a run of Backs inside one file step for step, back to where it began" { + const t = try JumpsTest.init("ABABA"); + defer t.p.deinit(); + var buf: [128]u8 = undefined; + // A1 B1 A2 B2 A3 from A3: nothing older than A1 at all, so Back ends there. + try std.testing.expectEqualStrings("A50 A10", try t.walk(.call, -1, &buf)); + try std.testing.expectEqual(@as(usize, 0), t.p.jcur); + try std.testing.expectEqualStrings("A50 A90", try t.walk(.call, 1, &buf)); + try std.testing.expectEqual(@as(usize, 4), t.p.jcur); + // One Back, one Forward: the same entry, whichever were passed over. + t.step(.call, -1); + try std.testing.expectEqual(@as(usize, 2), t.p.jcur); + t.step(.call, 1); + try std.testing.expectEqual(@as(usize, 4), t.p.jcur); + try std.testing.expectEqual(@as(usize, 5), t.p.njumps); +} + +test "JumpScope all: Back and Forward take every place in the order it was jumped to" { + const t = try JumpsTest.init("ABABA"); + defer t.p.deinit(); + try std.testing.expect(t.p.executeBuiltinLine(t.p.active, "JumpScope all")); + try std.testing.expectEqual(config.Runtime.JumpScope.all, t.p.settings.jump_scope); + var buf: [128]u8 = undefined; + try std.testing.expectEqualStrings("B50 A50 B10 A10", try t.walk(.call, -1, &buf)); + try std.testing.expectEqualStrings("B10 A50 B50 A90", try t.walk(.call, 1, &buf)); + // Bare, the word steps back to file. + try std.testing.expect(t.p.executeBuiltinLine(t.p.active, "JumpScope")); + try std.testing.expectEqual(config.Runtime.JumpScope.file, t.p.settings.jump_scope); +} + +test "JumpScope file: from a pane with no file, Back and Forward are global" { + const t = try JumpsTest.init("OAOBO"); + defer t.p.deinit(); + var buf: [128]u8 = undefined; + // O90 keeps to nothing: B10, the next older of all. From B, nothing of + // B is older, so O50; from O, A10; from A, O10. + try std.testing.expectEqualStrings("B10 O50 A10 O10", try t.walk(.call, -1, &buf)); + try std.testing.expectEqualStrings("A10 O50 B10 O90", try t.walk(.call, 1, &buf)); +} + +test "JumpScope file: a file in two panes keeps to the pane first, then the file in the other pane" { + // C is A's file in a second pane: C10 B10 A10 C50 A50. + const t = try JumpsTest.init("CBACA"); + defer t.p.deinit(); + var buf: [128]u8 = undefined; + // From A50: A10 in this pane, over the nearer C50; then none of A's + // own is older, so the file's in C, C10, over B10. + try std.testing.expectEqualStrings("A10 C10", try t.walk(.call, -1, &buf)); + // From C10 in C: C50, its own, then the file's newest, A50. + try std.testing.expectEqualStrings("C50 A50", try t.walk(.call, 1, &buf)); +} + +test "Back and Forward take the same steps from Ctrl-o and Ctrl-i, the mouse's side buttons, the builtins and jumpBy" { + for ([_]JumpsTest.Way{ .call, .key, .builtin, .mouse }) |way| { + const t = try JumpsTest.init("BABABA"); + defer t.p.deinit(); + var buf: [128]u8 = undefined; + try std.testing.expectEqualStrings("A50 A10 B10", try t.walk(way, -1, &buf)); + try std.testing.expectEqualStrings("B50 B90 A90", try t.walk(way, 1, &buf)); + } +} + test "jump history terminal rectangle follows output and clears on reflow" { if (comptime !terminal_panes) return error.SkipZigTest; const p = try Pardes.init(std.testing.allocator, .{ .cols = 60, .rows = 12, .tty_only = true }); diff --git a/src/tty/tty.zig b/src/tty/tty.zig index a067ffce..f0f41747 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -2452,6 +2452,9 @@ fn mouseEvent(m: vaxis.Mouse) ?pardes.Event { .wheel_down => .wheel_down, .wheel_left => .wheel_left, .wheel_right => .wheel_right, + // A mouse's side buttons, as xterm reports them: Back and Forward. + .button_8 => .back, + .button_9 => .forward, .none => .none, // button-less motion: hover tracking else => return null, }; @@ -2471,6 +2474,13 @@ fn mouseEvent(m: vaxis.Mouse) ?pardes.Event { } }; } +test "a mouse's side buttons, xterm's 8 and 9, are Back and Forward, as SDL's X1 and X2 are" { + for ([_]vaxis.Mouse.Button{ .button_8, .button_9 }, [_]pardes.Mouse.Button{ .back, .forward }) |from, to| { + const event = mouseEvent(.{ .col = 3, .row = 2, .button = from, .mods = .{}, .type = .press }).?; + try std.testing.expectEqual(to, event.mouse.button); + } +} + /// A blank whose ink alone changed is not sent: vaxis's render skips it /// (pardes's patch to Vaxis.zig in build.zig), since a blank's ink shows /// nowhere. Deciding that here cost every cell of every frame a look at the |
