summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 17:52:24 -0300
committerGabriel Schneider <[email protected]>2026-10-01 18:02:10 -0300
commitafec162464d360cbbf475d3fb7457cda83ce918f (patch)
tree7278bc32743769c096fd743758ea3a2967b8be93 /src/pardes.zig
parentfe9ec319e01052e3ede65ad0e8a809dd5f2c9953 (diff)
downloadpardes-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
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig203
1 files changed, 197 insertions, 6 deletions
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 });