summaryrefslogtreecommitdiff
path: root/test/panes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 00:01:57 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit23f430573efe4f6ef55b0180aa0d742ff4eca1d9 (patch)
treeda2157ba73d6abff4b1674541433294fb6c2daa9 /test/panes.zig
parent9584b0682b45f17aaa42157540947904d867777a (diff)
downloadpardes-23f430573efe4f6ef55b0180aa0d742ff4eca1d9.tar.gz
pardes-23f430573efe4f6ef55b0180aa0d742ff4eca1d9.zip
Make a pane's tag a Text edited with the body's own keys
The tag was a fixed buffer with its own one-line editor, a cursor measured across the path, and a hijack of the body's mode while it was typed into. It is now the pane's second Text: its own characters after the computed prefix (path, dirty marker, PDF page), which is never stored in it, so a rename or a dirty marker never moves its cursor. Normal and insert mode are the body's, undo included; `:` moves the keyboard between body and tag, the tag remembers its cursor and starts on Save, and executing a word from it hands the keyboard back. The mouse still sees the prefix: a sweep selects across it and a click on the path drafts a new name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'test/panes.zig')
-rw-r--r--test/panes.zig384
1 files changed, 201 insertions, 183 deletions
diff --git a/test/panes.zig b/test/panes.zig
index f1adafee..a391dba2 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -12,6 +12,17 @@ const image = pardes.image;
const syntax = pardes.syntax;
const filesystem = pardes.filesystem;
+/// The tag's caret as a byte offset into the tag as it is shown, prefix
+/// included.
+fn tagCaret(p: *Pardes, pane: *Pane) !usize {
+ return (try p.tagPrefix(pane)).len + @as(usize, @intCast(pane.tag.cur_col));
+}
+
+fn setTag(pane: *Pane, text: []const u8) !void {
+ if (pane.tag.own) |own| pane.gpa.free(own);
+ pane.tag.own = try pane.gpa.dupe(u8, text);
+}
+
const TagLayerTests = struct {
const Kind = @FieldType(pardes.TagLayer, "kind");
fn findLayer(p: *Pardes, kind: Kind, id: usize) !*pardes.TagLayer {
@@ -36,10 +47,7 @@ const TagLayerTests = struct {
p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 };
p.gpa.free(pane.file.?.path);
pane.file.?.path = try p.gpa.dupe(u8, "/x");
- const tail = " " ++ "a" ** 38 ++ "\t界 END";
- @memcpy(pane.tag_tail[0..tail.len], tail);
- pane.tag_tail_len = tail.len;
- pane.tag_init = true;
+ try setTag(pane, " " ++ "a" ** 38 ++ "\t界 END");
var frame = std.heap.ArenaAllocator.init(p.gpa);
defer frame.deinit();
_ = try p.render(frame.allocator());
@@ -55,11 +63,11 @@ const TagLayerTests = struct {
var stale = mouse(layer, x, .press);
stale.tag_hit.?.serial +%= 1;
p.update(.{ .mouse = stale });
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.focus == .body);
p.update(.{ .mouse = mouse(layer, x, .press) });
p.update(.{ .mouse = mouse(layer, x, .release) });
- try std.testing.expect(pane.tag_edit);
- try std.testing.expectEqual(@as(u16, @intCast(at)), pane.tag_col);
+ try std.testing.expect(pane.focus == .tag);
+ try std.testing.expectEqual(at, try tagCaret(p, pane));
p.update(.{ .key = .{ .cp = 'X', .text = "X" } });
_ = try p.render(frame.allocator());
const edited = try p.tagText(frame.allocator(), pane);
@@ -91,7 +99,7 @@ const TagLayerTests = struct {
event.row = @intFromFloat(painted_y);
p.update(.{ .mouse = event });
}
- try std.testing.expectEqual(@as(u16, @intCast(at)), pane.tag_col);
+ try std.testing.expectEqual(at, try tagCaret(p, pane));
}
}
@@ -119,19 +127,17 @@ const TagLayerTests = struct {
try std.testing.expectEqualStrings(" ", p.surface.at(layer.viewport.x - 1, layer.viewport.y).grapheme());
p.update(.{ .mouse = mouse(layer, edge - 0.1, .press) });
try std.testing.expect(p.drag != .move);
- try std.testing.expect(!p.panes[second].?.tag_edit);
+ try std.testing.expect(p.panes[second].?.focus == .body);
p.update(.{ .mouse = mouse(layer, edge - 0.1, .release) });
p.update(.{ .mouse = mouse(layer, edge + 0.1, .press) });
p.update(.{ .mouse = mouse(layer, edge + 0.1, .release) });
try std.testing.expectEqual(second, p.active);
- try std.testing.expect(p.panes[second].?.tag_edit);
- try std.testing.expectEqual(@as(u16, 0), p.panes[second].?.tag_col);
+ // The first cell of the tag is its path: a click there drafts a name.
+ try std.testing.expect(p.panes[second].?.prompt == .name);
+ try std.testing.expectEqual(@as(i32, 0), p.panes[second].?.input.cur_col);
p.update(.{ .key = .{ .cp = Key.escape } });
const left = p.panes[0].?;
- const tail = " " ++ "a" ** 110;
- @memcpy(left.tag_tail[0..tail.len], tail);
- left.tag_tail_len = tail.len;
- left.tag_init = true;
+ try setTag(left, " " ++ "a" ** 110);
_ = try p.render(frame.allocator());
const left_layer = try findLayer(p, .pane, 0);
const left_text = try p.tagText(frame.allocator(), left);
@@ -142,8 +148,8 @@ const TagLayerTests = struct {
p.update(.{ .mouse = mouse(layer, beyond_x, .release) });
try std.testing.expectEqual(@as(usize, 0), p.active);
const end_col = left_layer.capacity(p.row_metrics) - 1;
- try std.testing.expectEqual(@as(u16, @intCast(panes.File.rawAtDisplay(left_text, end_col))), left.tag_col);
- try std.testing.expect(left.tag_sel);
+ try std.testing.expectEqual(panes.File.rawAtDisplay(left_text, end_col), try tagCaret(p, left));
+ try std.testing.expect(left.sel[0].state == .done and left.sel[0].r0 == 0);
p.update(.{ .key = .{ .cp = Key.escape } });
const long = "a" ** 60 ++ "界END";
try std.testing.expect(p.global_tag.set(long));
@@ -167,12 +173,12 @@ const TagNavigationTests = struct {
p.update(.{ .key = .{ .cp = cp } });
}
- fn enterNormal(p: *Pardes) *Pane {
- key(p, ':');
- return p.panes[p.active].?;
+ fn window(p: *Pardes, letter: u21) void {
+ p.update(.{ .key = .{ .cp = 'w', .ctrl = true } });
+ key(p, letter);
}
- test "tag navigation retains each pane cursor and mouse placement overrides memory" {
+ test "a tag remembers its cursor, starts on Save, and a click moves it" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 });
defer p.deinit();
const first = try p.setTestFile("body\n");
@@ -183,65 +189,97 @@ const TagNavigationTests = struct {
try std.testing.expect(layout.splitColumn(p, second_id, second_id, false));
p.update(.tick);
p.active = 0;
+ // Nothing to remember yet: `:` lands on Save, in normal mode.
key(p, ':');
+ try std.testing.expect(first.focus == .tag and first.tag.mode == .normal);
+ const tail = Pardes.curTail(first);
+ try std.testing.expectEqualStrings("Save", tail[@intCast(first.tag.cur_col)..][0..4]);
key(p, Key.home);
for (0..3) |_| key(p, Key.right);
- const first_col = first.tag_col;
- key(p, Key.escape);
+ try std.testing.expectEqual(@as(i32, 3), first.tag.cur_col);
+ // `:` in the tag's normal mode is the way back to the body.
+ key(p, ':');
+ try std.testing.expect(first.focus == .body);
p.active = second_id;
key(p, ':');
key(p, Key.home);
for (0..5) |_| key(p, Key.right);
- const second_col = second.tag_col;
- key(p, Key.escape);
+ key(p, ':');
p.active = 0;
key(p, ':');
- try std.testing.expectEqual(first_col, first.tag_col);
- key(p, Key.escape);
+ try std.testing.expectEqual(@as(i32, 3), first.tag.cur_col);
+ key(p, ':');
p.active = second_id;
key(p, ':');
- try std.testing.expectEqual(second_col, second.tag_col);
- key(p, Key.escape);
+ try std.testing.expectEqual(@as(i32, 5), second.tag.cur_col);
+ key(p, ':');
var frame = std.heap.ArenaAllocator.init(p.gpa);
defer frame.deinit();
_ = try p.render(frame.allocator());
const rect = p.rects[0];
const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
- ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + 6, y);
+ const prefix: u16 = @intCast(panes.File.displayWidth(try p.tagPrefix(first)));
+ ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + prefix + 6, y);
try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expectEqual(@as(u16, 6), first.tag_col);
+ try std.testing.expect(first.focus == .tag and first.tag.mode == .insert);
+ try std.testing.expectEqual(@as(i32, 6), first.tag.cur_col);
key(p, Key.escape);
+ key(p, ':');
p.startPrompt(first, .{ .search = config.search_marker });
key(p, Key.escape);
key(p, ':');
- try std.testing.expectEqual(@as(u16, 6), first.tag_col);
- key(p, Key.escape);
+ try std.testing.expectEqual(@as(i32, 6), first.tag.cur_col);
+ key(p, ':');
try std.testing.expectEqualStrings("body\n", first.file.?.content);
}
- test "tag navigation clamps remembered columns to renamed graphemes and shorter tags" {
+ test "executing a word from the tag runs it and gives the keyboard back to the body" {
+ for ([_]u21{ Key.tab, Key.enter }) |chord| {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 });
+ defer p.deinit();
+ const pane = try p.setTestFile("body\n");
+ p.update(.{ .key = .{ .cp = 'i' } });
+ p.update(.{ .key = .{ .cp = 'x', .text = "x" } });
+ p.update(.{ .key = .{ .cp = Key.escape } });
+ while (p.nextEffect()) |_| {}
+ // `:` lands on Save; the execute key runs it, the look key looks
+ // it up. Either way the tag lets go of the keyboard first.
+ key(p, ':');
+ try std.testing.expect(pane.focus == .tag);
+ key(p, chord);
+ try std.testing.expect(pane.focus == .body);
+ try std.testing.expectEqual(.normal, pane.body.mode);
+ var saved = false;
+ while (p.nextEffect()) |effect| {
+ if (effect == .save_file) saved = true;
+ }
+ try std.testing.expectEqual(chord == Key.tab, saved);
+ // ...and the tag still knows where it was.
+ key(p, ':');
+ try std.testing.expectEqualStrings("Save", Pardes.curTail(pane)[@intCast(pane.tag.cur_col)..][0..4]);
+ }
+ }
+
+ test "a renamed path never moves the tag's cursor and a shorter tag pulls it back" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 14 });
defer p.deinit();
const pane = try p.setTestFile("body\n");
key(p, ':');
key(p, Key.home);
for (0..3) |_| key(p, Key.right);
- key(p, Key.escape);
+ key(p, ':');
p.gpa.free(pane.file.?.path);
pane.file.?.path = try p.gpa.dupe(u8, "/界.zig");
key(p, ':');
- try std.testing.expectEqual(@as(u16, 1), pane.tag_col);
- key(p, Key.end);
- key(p, Key.escape);
- p.gpa.free(pane.file.?.path);
- pane.file.?.path = try p.gpa.dupe(u8, "/é");
- pane.tag_tail_len = 0;
+ try std.testing.expectEqual(@as(i32, 3), pane.tag.cur_col);
+ key(p, ':');
+ try setTag(pane, "é");
key(p, ':');
- try std.testing.expectEqual(@as(u16, 3), pane.tag_col);
+ try std.testing.expectEqual(@as(i32, 2), pane.tag.cur_col);
try std.testing.expectEqualStrings("body\n", pane.file.?.content);
}
- test "tag navigation J and K restore cursors in stacked panes" {
+ test "window keys move between panes from a tag and each tag keeps its cursor" {
for ([_]bool{ false, true }) |bottom| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
@@ -253,88 +291,67 @@ const TagNavigationTests = struct {
const second = p.panes[second_id].?;
p.update(.tick);
p.active = 0;
- _ = enterNormal(p);
+ key(p, ':');
key(p, Key.home);
key(p, 'l');
key(p, 'l');
- key(p, 'J');
+ window(p, 'j');
try std.testing.expectEqual(second_id, p.active);
- try std.testing.expect(!first.tag_edit and second.tag_edit);
+ try std.testing.expect(first.focus == .body and second.focus == .body);
+ key(p, ':');
key(p, Key.home);
for (0..4) |_| key(p, 'l');
- key(p, 'K');
+ window(p, 'k');
try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expectEqual(@as(u16, 2), first.tag_col);
- key(p, 'J');
- try std.testing.expectEqual(second_id, p.active);
- try std.testing.expectEqual(@as(u16, 4), second.tag_col);
+ try std.testing.expect(second.focus == .body);
+ key(p, ':');
+ try std.testing.expectEqual(@as(i32, 2), first.tag.cur_col);
+ window(p, 'j');
+ key(p, ':');
+ try std.testing.expectEqual(@as(i32, 4), second.tag.cur_col);
}
}
- test "tag navigation uses uppercase pane and header keys while lowercase stays local" {
+ test "a tag's normal mode is the body's: motions stay in the tag and insert types into it" {
for ([_]bool{ false, true }) |bottom| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 });
defer p.deinit();
_ = try p.setTestFile("body\n");
p.presentation.enabled = false;
p.settings.tag_bottom = bottom;
- p.settings.column_tags = true;
try std.testing.expect(p.executeBuiltinLine(0, "New"));
const right = p.active;
try std.testing.expect(layout.splitColumn(p, right, right, false));
p.update(.tick);
p.active = 0;
- const pane = enterNormal(p);
+ key(p, ':');
+ const pane = p.panes[0].?;
key(p, Key.home);
key(p, 'l');
- try std.testing.expectEqual(@as(u16, 1), pane.tag_col);
+ try std.testing.expectEqual(@as(i32, 1), pane.tag.cur_col);
key(p, 'h');
key(p, 'k');
- try std.testing.expectEqual(@as(u16, 0), pane.tag_col);
+ key(p, 'j');
+ try std.testing.expectEqual(@as(i32, 0), pane.tag.cur_col);
try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expect(pane.tag_edit and p.topbar_col == null);
- key(p, 'L');
+ try std.testing.expect(pane.focus == .tag and p.topbar_col == null);
+ window(p, 'l');
try std.testing.expectEqual(right, p.active);
- try std.testing.expect(!pane.tag_edit and p.panes[right].?.tag_edit);
- key(p, 'H');
+ try std.testing.expect(pane.focus == .body and p.panes[right].?.focus == .body);
+ window(p, 'h');
try std.testing.expectEqual(@as(usize, 0), p.active);
- key(p, 'K');
- try std.testing.expect(p.topbar_col != null and p.header_column != null);
- try std.testing.expect(!pane.tag_edit);
- key(p, 'l');
- const column_cursor = p.topbar_col.?;
- key(p, 'K');
- try std.testing.expect(p.topbar_col != null and p.header_column == null);
- key(p, 'l');
- key(p, 'l');
- const workspace_cursor = p.topbar_col.?;
- key(p, 'J');
- try std.testing.expect(p.topbar_col != null and p.header_column != null);
- try std.testing.expectEqual(column_cursor, p.topbar_col.?);
- key(p, 'K');
- try std.testing.expectEqual(workspace_cursor, p.topbar_col.?);
- key(p, 'J');
- key(p, 'J');
- try std.testing.expect(p.topbar_col == null and pane.tag_edit);
- key(p, 'j');
- try std.testing.expect(!pane.tag_edit and p.topbar_col == null);
- try std.testing.expectEqual(@as(usize, 0), p.active);
- _ = enterNormal(p);
- key(p, 'K');
- key(p, 'j');
- try std.testing.expect(p.topbar_col == null and !pane.tag_edit);
key(p, ':');
key(p, 'i');
key(p, Key.end);
- const before = pane.tag_tail_len;
- try std.testing.expect(pane.tag_edit and pane.body.mode == .insert);
- for ("hjklHJKL") |letter| {
+ const before = Pardes.curTail(pane).len;
+ try std.testing.expect(pane.focus == .tag and pane.tag.mode == .insert and pane.body.mode == .normal);
+ for ("hjklHJKL:") |letter| {
const text = [_]u8{letter};
p.update(.{ .key = .{ .cp = letter, .text = &text } });
- try std.testing.expect(pane.tag_edit and pane.body.mode == .insert);
+ try std.testing.expect(pane.focus == .tag and pane.tag.mode == .insert);
}
- try std.testing.expectEqualStrings("hjklHJKL", pane.tag_tail[before..pane.tag_tail_len]);
- try std.testing.expect(pane.tag_edit and pane.body.mode == .insert and p.topbar_col == null);
+ try std.testing.expectEqualStrings("hjklHJKL:", Pardes.curTail(pane)[before..]);
+ try std.testing.expectEqualStrings("body\n", pane.file.?.content);
try std.testing.expectEqual(@as(usize, 0), p.active);
}
}
@@ -1151,32 +1168,26 @@ const TagNameTintTests = struct {
}
}
- test "tag filename tint follows scrolled Unicode staged names and yields to selection" {
+ test "tag filename tint follows scrolled Unicode drafted names and yields to selection" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 38, .rows = 14 });
defer p.deinit();
const pane = try p.setTestFile("body\n");
- const old = pane.file.?.path.len;
configure(p);
defer p.custom_theme = null;
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(old - 1);
- pane.tag_sel = true;
const name = "/a/very/long/directory/that/extends/past/the/tag/界e\u{301}name-with-a-long-tail.zig";
- p.update(.{ .paste = name });
- try std.testing.expectEqualStrings(name, pane.tag_name[0..pane.tag_name_len.?]);
- pane.tag_col = @intCast(std.mem.indexOf(u8, name, "界").?);
- pane.tag_sel = false;
+ p.draftName(pane, 0);
+ redraft(pane, name);
+ pane.input.cur_col = @intCast(std.mem.indexOf(u8, name, "界").?);
try std.testing.expect((try check(p, 0, name)) > 0);
try std.testing.expect(pane.tag_scroll > 0);
- pane.tag_anchor = pane.tag_col;
- pane.tag_sel = true;
+ // A sweep over the wide glyph wears the selection over the tint.
+ const col: i32 = @intCast(panes.File.rawDisplayCol(name, @intCast(pane.input.cur_col)));
+ pane.sel[0] = .{ .state = .done, .c0 = col, .c1 = col + 1, .r0 = 0, .r1 = 0 };
pane.tag_scroll += 4; // bring both cells of the selected wide glyph into view
var arena = std.heap.ArenaAllocator.init(p.gpa);
defer arena.deinit();
const surface = try p.render(arena.allocator());
- const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.rawDisplayCol(name, pane.tag_col) - pane.tag_scroll));
+ const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col - pane.tag_scroll));
const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y;
try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x, y).style.fg);
try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x + 1, y).style.fg);
@@ -1208,32 +1219,46 @@ const TagNameTintTests = struct {
}
};
-test "pane filename edits stage commit and cancel without changing body or command tail" {
+/// Replace a name draft's text, the caret after it, as clearing it and
+/// typing would.
+fn redraft(pane: *Pane, name: []const u8) void {
+ if (pane.input.own) |own| pane.gpa.free(own);
+ pane.input.own = pane.gpa.dupe(u8, name) catch unreachable;
+ pane.input.cur_col = @intCast(name.len);
+}
+
+/// Click the tagline at the end of the path: a name draft, caret there.
+fn clickPathEnd(p: *Pardes, pane: *Pane) void {
+ const rect = p.rects[p.active];
+ const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
+ const x = rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(pane.file.?.path) - 1));
+ ClickCursorTests.click(p, .left, x, y);
+ std.debug.assert(pane.prompt == .name);
+ pane.input.cur_col = @intCast(pane.input.own.?.len);
+}
+
+test "pane filename drafts commit and cancel without changing body or command tail" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 70, .rows = 12 });
defer p.deinit();
const pane = try p.setTestFile("original body\n");
const original = try p.gpa.dupe(u8, pane.file.?.path);
defer p.gpa.free(original);
- p.seedTail(pane);
- try std.testing.expect(pane.appendTag(" SyntaxBold"));
- const tail = try p.gpa.dupe(u8, pane.tagSlice());
+ try pane.appendTag(" SyntaxBold");
+ const tail = try p.gpa.dupe(u8, Pardes.curTail(pane));
defer p.gpa.free(tail);
for ([_]bool{ false, true }) |commit| {
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(original.len - 1);
- pane.tag_sel = true;
+ clickPathEnd(p, pane);
+ p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
p.update(.{ .paste = "/new name.zig" });
try std.testing.expectEqualStrings(original, pane.file.?.path);
- try std.testing.expectEqualStrings("/new name.zig", pane.tag_name[0..pane.tag_name_len.?]);
- try std.testing.expectEqualStrings(tail, pane.tagSlice());
+ try std.testing.expectEqualStrings("/new name.zig", pane.input.own.?);
+ try std.testing.expectEqualStrings(tail, Pardes.curTail(pane));
try std.testing.expectEqualStrings("original body\n", pane.file.?.content);
p.update(.{ .key = .{ .cp = if (commit) Key.enter else Key.escape } });
- try std.testing.expect(!pane.tag_edit);
- try std.testing.expect(pane.tag_name_len == null);
+ try std.testing.expect(pane.focus == .body);
+ try std.testing.expect(pane.prompt == .none);
try std.testing.expectEqualStrings(if (commit) "/new name.zig" else original, pane.file.?.path);
- try std.testing.expectEqualStrings(tail, pane.tagSlice());
+ try std.testing.expectEqualStrings(tail, Pardes.curTail(pane));
while (p.nextEffect()) |effect| try std.testing.expect(std.meta.activeTag(effect) != .save_file);
}
try std.testing.expect(pane.file.?.revision != pane.file.?.saved_revision);
@@ -1243,27 +1268,21 @@ test "pane filename grapheme edits and invalid empty commits preserve the buffer
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
const pane = try p.setTestFile("untouched\n");
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(pane.file.?.path.len - 1);
- pane.tag_sel = true;
+ clickPathEnd(p, pane);
+ p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
p.update(.{ .paste = "/café界" });
p.update(.{ .key = .{ .cp = Key.backspace } });
- try std.testing.expectEqualStrings("/café", pane.tag_name[0..pane.tag_name_len.?]);
+ try std.testing.expectEqualStrings("/café", pane.input.own.?);
p.update(.{ .key = .{ .cp = Key.backspace } });
- try std.testing.expectEqualStrings("/caf", pane.tag_name[0..pane.tag_name_len.?]);
- pane.tag_anchor = 0;
- pane.tag_col = 3;
- pane.tag_sel = true;
- p.update(.{ .key = .{ .cp = Key.delete } });
- try std.testing.expectEqual(@as(u16, 0), pane.tag_name_len.?);
+ try std.testing.expectEqualStrings("/caf", pane.input.own.?);
+ p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
+ try std.testing.expectEqualStrings("", pane.input.own.?);
p.update(.{ .key = .{ .cp = Key.enter } });
- try std.testing.expect(pane.tag_edit);
+ try std.testing.expect(pane.prompt == .name);
try std.testing.expectEqualStrings("/test.txt", pane.file.?.path);
try std.testing.expectEqualStrings("untouched\n", pane.file.?.content);
p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(pane.tag_name_len == null);
+ try std.testing.expect(pane.prompt == .none);
}
test "leaving a pane or entering a header cancels an uncommitted filename" {
@@ -1273,11 +1292,10 @@ test "leaving a pane or entering a header cancels an uncommitted filename" {
for ([_]bool{ false, true }) |header| {
p.active = 0;
p.topbar_col = null;
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- p.update(.{ .key = .{ .cp = Key.home } });
+ p.update(.tick);
+ clickPathEnd(p, pane);
p.update(.{ .paste = "/draft" });
- try std.testing.expect(pane.tag_name_len != null);
+ try std.testing.expect(pane.prompt == .name);
if (header) {
p.topbar_col = 0;
p.update(.tick);
@@ -1285,21 +1303,18 @@ test "leaving a pane or entering a header cancels an uncommitted filename" {
try std.testing.expect(p.executeBuiltinLine(0, "New"));
p.update(.tick);
}
- try std.testing.expect(pane.tag_name_len == null);
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.prompt == .none);
+ try std.testing.expect(pane.focus == .body);
try std.testing.expectEqualStrings("/test.txt", pane.file.?.path);
}
}
-test "middle clicking Save commits the staged filename before issuing a save" {
+test "middle clicking Save commits the drafted filename before issuing a save" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 70, .rows = 12 });
defer p.deinit();
const pane = try p.setTestFile("untouched\n");
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(pane.file.?.path.len - 1);
- pane.tag_sel = true;
+ clickPathEnd(p, pane);
+ p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
p.update(.{ .paste = "/renamed.txt" });
var arena = std.heap.ArenaAllocator.init(p.gpa);
defer arena.deinit();
@@ -1312,7 +1327,7 @@ test "middle clicking Save commits the staged filename before issuing a save" {
p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .press } });
p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .release } });
try std.testing.expectEqualStrings("/renamed.txt", pane.file.?.path);
- try std.testing.expect(pane.tag_name_len == null);
+ try std.testing.expect(pane.prompt == .none);
try std.testing.expectEqualStrings("untouched\n", pane.file.?.content);
var saves: usize = 0;
while (p.nextEffect()) |effect| if (effect == .save_file) {
@@ -1322,24 +1337,19 @@ test "middle clicking Save commits the staged filename before issuing a save" {
try std.testing.expectEqual(@as(usize, 1), saves);
}
-test "oversized filename paste refuses atomically across a selected tag boundary" {
+test "an oversized drafted name is refused at commit and the draft stays to correct" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
const pane = try p.setTestFile("untouched\n");
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- const before = try p.tagText(p.gpa, pane);
- defer p.gpa.free(before);
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(before.len - 1);
- pane.tag_sel = true;
- const excess = try p.gpa.alloc(u8, pane.tag_name.len + 1);
+ clickPathEnd(p, pane);
+ const excess = try p.gpa.alloc(u8, 5000);
defer p.gpa.free(excess);
@memset(excess, 'x');
+ excess[0] = '/';
p.update(.{ .paste = excess });
- try std.testing.expect(pane.tag_name_len == null);
- try std.testing.expect(pane.tag_sel);
- try std.testing.expectEqualStrings(before, try p.tagText(p.scratch.allocator(), pane));
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ try std.testing.expect(pane.prompt == .name);
+ try std.testing.expectEqualStrings("/test.txt", pane.file.?.path);
try std.testing.expectEqualStrings("untouched\n", pane.file.?.content);
}
@@ -1349,16 +1359,17 @@ test "paste into pane commands and prompts never inserts into the body" {
const pane = try p.setTestFile("one two one\n");
p.update(.{ .key = .{ .cp = ':' } });
p.update(.{ .key = .{ .cp = 'A' } });
- p.update(.{ .paste = " Custom\r\nCommand" });
- try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), " CustomCommand"));
+ // A tag takes a paste as a body does, newline and all.
+ p.update(.{ .paste = " Custom\nCommand" });
+ try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null);
p.update(.{ .key = .{ .cp = Key.escape } });
p.startPrompt(pane, .{ .search = config.search_marker });
p.update(.{ .paste = "t\r\nwo" });
try std.testing.expectEqualStrings(config.search_marker ++ "two", pane.promptText().?);
- try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), " CustomCommand"));
+ try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null);
try std.testing.expectEqualStrings("one two one\n", pane.file.?.content);
p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), " CustomCommand"));
+ try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null);
}
test "long pane tags reveal commands and map visible clicks to whole tag addresses" {
@@ -1367,8 +1378,7 @@ test "long pane tags reveal commands and map visible clicks to whole tag address
const pane = try p.setTestFile("body\n");
p.gpa.free(pane.file.?.path);
pane.file.?.path = try p.gpa.dupe(u8, "/an/especially/deep/project/path/with/a/long/component/filename.zig");
- p.seedTail(pane);
- try std.testing.expect(pane.appendTag(" SyntaxBold"));
+ try pane.appendTag(" SyntaxBold");
p.update(.{ .key = .{ .cp = ':' } });
p.update(.{ .key = .{ .cp = 'A' } });
var arena = std.heap.ArenaAllocator.init(p.gpa);
@@ -1549,7 +1559,13 @@ test "terminal edits and undo survive partial history clearing and whole-screen
pane.body.cur_pinned = true;
pane.body.vsel = .{ .active = true, .row = row, .col = 1 };
panes.Terminal.pushUndo(p, pane);
- pane.body.ed_redo[0] = panes.Terminal.snap(p, pane) orelse return error.SnapshotFailed;
+ const overlay = pane.ovl.?;
+ pane.body.ed_redo[0] = .{
+ .ovl = .{ .row = overlay.row, .rows = overlay.rows, .text = try gpa.dupe(u8, overlay.text) },
+ .cur_row = pane.body.cur_row,
+ .cur_col = pane.body.cur_col,
+ .vsel = pane.body.vsel,
+ };
pane.body.ed_redo_len = 1;
if (action == .hidden_undo) {
gpa.free(pane.ovl.?.text);
@@ -1663,9 +1679,7 @@ test "a prompt's entry and cancellation leave the tag, the body and its cursor a
try pane.setOwnedCwd("/dír space");
const tail = " keep";
for (cases) |case| {
- @memcpy(pane.tag_tail[0..tail.len], tail);
- pane.tag_tail_len = tail.len;
- pane.tag_init = true;
+ try setTag(pane, tail);
pane.body.cur_row = 1;
pane.body.cur_col = 2;
pane.body.cur_pinned = true;
@@ -1677,17 +1691,17 @@ test "a prompt's entry and cancellation leave the tag, the body and its cursor a
p.startPrompt(pane, .{ .search = config.search_marker });
}
// The answer is typed into the prompt's own line, never the tag.
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.focus == .body);
try std.testing.expectEqual(.insert, pane.input.mode);
try std.testing.expectEqualStrings(case.marker, pane.promptText().?);
try std.testing.expectEqual(@as(i32, @intCast(case.marker.len)), pane.input.cur_col);
- try std.testing.expectEqualStrings(tail, pane.tag_tail[0..pane.tag_tail_len]);
+ try std.testing.expectEqualStrings(tail, pane.tag.own.?);
try std.testing.expectEqualDeep(modal.Normal.State{}, pane.body.normal);
p.update(.{ .key = .{ .cp = Key.escape } });
try std.testing.expect(pane.prompt == .none);
try std.testing.expect(pane.input.own == null);
try std.testing.expectEqual(.normal, pane.body.mode);
- try std.testing.expectEqualStrings(tail, pane.tag_tail[0..pane.tag_tail_len]);
+ try std.testing.expectEqualStrings(tail, pane.tag.own.?);
try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row);
try std.testing.expectEqual(@as(i32, 2), pane.body.cur_col);
try std.testing.expectEqualStrings("one\ntwo\n", pane.file.?.content);
@@ -1698,9 +1712,10 @@ test "a prompt opens whatever the tag already holds" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
const pane = try p.setTestFile("one\n");
- @memset(&pane.tag_tail, 'x');
- pane.tag_tail_len = pane.tag_tail.len;
- pane.tag_init = true;
+ const full = try p.gpa.alloc(u8, pardes.memory.limits.max_tag_tail);
+ defer p.gpa.free(full);
+ @memset(full, 'x');
+ try setTag(pane, full);
for (0..3) |case| {
switch (case) {
0 => p.startPrompt(pane, .{ .search = config.search_marker }),
@@ -1708,8 +1723,7 @@ test "a prompt opens whatever the tag already holds" {
else => p.update(.{ .key = .{ .cp = '|' } }),
}
try std.testing.expect(pane.prompt != .none);
- try std.testing.expectEqual(pane.tag_tail.len, pane.tag_tail_len);
- for (pane.tag_tail) |byte| try std.testing.expectEqual(@as(u8, 'x'), byte);
+ try std.testing.expectEqualStrings(full, pane.tag.own.?);
p.update(.{ .key = .{ .cp = Key.escape } });
}
}
@@ -1723,8 +1737,8 @@ test "generated output refuses every pipe prompt before seeding the tag" {
for ([_]Key{ .{ .cp = '|' }, .{ .cp = '|', .alt = true }, .{ .cp = '!' }, .{ .cp = '!', .alt = true } }) |key| {
p.update(.{ .key = key });
try std.testing.expect(pane.prompt == .none);
- try std.testing.expect(!pane.tag_init);
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.tag.own == null);
+ try std.testing.expect(pane.focus == .body);
try std.testing.expectEqualStrings("generated\n", pane.file.?.content);
}
}
@@ -1798,7 +1812,7 @@ test "a prompt that cannot be opened leaves the open one as it was" {
try std.testing.expect(pane.prompt == .search);
try std.testing.expectEqualStrings(config.search_marker ++ "prior", pane.promptText().?);
try std.testing.expectEqualDeep(previous_normal, pane.body.normal);
- try std.testing.expect(!pane.tag_init and !pane.tag_edit);
+ try std.testing.expect(pane.tag.own == null and pane.focus == .body);
}
}
@@ -1830,7 +1844,7 @@ test "a prompt opens with one allocation" {
const before = failing.alloc_index;
p.startPrompt(pane, request);
try std.testing.expectEqual(before + 1, failing.alloc_index);
- try std.testing.expect(!pane.tag_edit and pane.input.mode == .insert);
+ try std.testing.expect(pane.focus == .body and pane.input.mode == .insert);
}
}
@@ -4080,14 +4094,18 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu
const rect = p.rects[0];
const tag_y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
const body_y = p.bodyTop(rect);
- const x = rect.x + config.GUTTER + @as(u16, @intCast(panes.File.gutterWidth(pane))) + 3;
+ const x = if (gesture == .tag)
+ rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(try p.tagPrefix(pane)))) + 3
+ else
+ rect.x + config.GUTTER + @as(u16, @intCast(panes.File.gutterWidth(pane))) + 3;
const y = if (gesture == .tag) tag_y else body_y;
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = x, .row = y } });
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = x, .row = y } });
if (gesture == .tag) {
- try std.testing.expect(pane.tag_edit);
+ try std.testing.expect(pane.focus == .tag);
p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(!pane.tag_edit);
+ p.update(.{ .key = .{ .cp = ':' } });
+ try std.testing.expect(pane.focus == .body);
}
},
.focus, .last => {