summaryrefslogtreecommitdiff
path: root/test/panes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'test/panes.zig')
-rw-r--r--test/panes.zig220
1 files changed, 206 insertions, 14 deletions
diff --git a/test/panes.zig b/test/panes.zig
index 7d7a9ab7..2d57ecd8 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -12,6 +12,185 @@ const image = pardes.image;
const syntax = pardes.syntax;
const filesystem = pardes.filesystem;
+test "pane filename edits stage 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());
+ 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;
+ 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("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.expectEqualStrings(if (commit) "/new name.zig" else original, pane.file.?.path);
+ try std.testing.expectEqualStrings(tail, pane.tagSlice());
+ 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);
+}
+
+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;
+ p.update(.{ .paste = "/café界" });
+ p.update(.{ .key = .{ .cp = Key.backspace } });
+ try std.testing.expectEqualStrings("/café", pane.tag_name[0..pane.tag_name_len.?]);
+ 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.?);
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ try std.testing.expect(pane.tag_edit);
+ 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);
+}
+
+test "leaving a pane or entering a header cancels an uncommitted filename" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("untouched\n");
+ 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(.{ .paste = "/draft" });
+ try std.testing.expect(pane.tag_name_len != null);
+ if (header) {
+ p.topbar_col = 0;
+ p.update(.tick);
+ } else {
+ 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.expectEqualStrings("/test.txt", pane.file.?.path);
+ }
+}
+
+test "middle clicking Save commits the staged 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;
+ p.update(.{ .paste = "/renamed.txt" });
+ var arena = std.heap.ArenaAllocator.init(p.gpa);
+ defer arena.deinit();
+ _ = try p.render(arena.allocator());
+ const text = try p.tagText(arena.allocator(), pane);
+ const save_at = std.mem.indexOf(u8, text, "Save").?;
+ const x: u16 = @intCast(p.rects[0].x + config.GUTTER + save_at - pane.tag_scroll);
+ const y = p.rects[0].y;
+ while (p.nextEffect()) |_| {}
+ 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.expectEqualStrings("untouched\n", pane.file.?.content);
+ var saves: usize = 0;
+ while (p.nextEffect()) |effect| if (effect == .save_file) {
+ try std.testing.expectEqual(@as(u8, 0), effect.save_file.pane);
+ saves += 1;
+ };
+ try std.testing.expectEqual(@as(usize, 1), saves);
+}
+
+test "oversized filename paste refuses atomically across a selected tag boundary" {
+ 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);
+ defer p.gpa.free(excess);
+ @memset(excess, 'x');
+ 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));
+ try std.testing.expectEqualStrings("untouched\n", pane.file.?.content);
+}
+
+test "paste into pane commands and prompts never inserts into the body" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ 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"));
+ p.update(.{ .key = .{ .cp = Key.escape } });
+ p.startPrompt(pane, .{ .search = config.search_marker });
+ p.update(.{ .paste = "two" });
+ try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), "two"));
+ 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"));
+}
+
+test "long pane tags reveal commands and map visible clicks to whole tag addresses" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 50, .rows = 12 });
+ defer p.deinit();
+ 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"));
+ p.update(.{ .key = .{ .cp = ':' } });
+ p.update(.{ .key = .{ .cp = 'A' } });
+ var arena = std.heap.ArenaAllocator.init(p.gpa);
+ defer arena.deinit();
+ const surface = try p.render(arena.allocator());
+ try std.testing.expect(pane.tag_scroll > 0);
+ try std.testing.expect(surface.cursor.?.x < p.rects[0].x + p.rects[0].w);
+ const text = try p.tagText(p.gpa, pane);
+ defer p.gpa.free(text);
+ const offset = std.mem.indexOf(u8, text, "SyntaxBold").?;
+ const x: u16 = @intCast(p.rects[0].x + config.GUTTER + panes.File.rawDisplayCol(text, offset) - pane.tag_scroll);
+ const y = p.rects[0].y;
+ const before = p.settings.syntax_bold;
+ 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.expectEqual(!before, p.settings.syntax_bold);
+ try std.testing.expectEqualStrings(text, try p.tagText(p.scratch.allocator(), pane));
+}
+
test "terminal overlay recoloring never materializes unrelated history" {
const testing = std.testing;
const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 32, .rows = 8 });
@@ -37,7 +216,7 @@ test "terminal overlay recoloring never materializes unrelated history" {
const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
const cell = surface.at(rect.x + config.GUTTER, body_y);
try testing.expectEqualStrings("c", cell.grapheme());
- try testing.expectEqual(pardes.Color{ .index = 2 }, cell.style.fg);
+ try testing.expectEqual(pardes.Color{ .rgb = p.theme().palette.?[2] }, cell.style.fg);
}
test "terminal output keeps following new rows after scrollback eviction" {
@@ -86,7 +265,7 @@ test "terminal output keeps following new rows after scrollback eviction" {
for (last, 0..) |byte, col| {
const cell = surface.at(x + @as(u16, @intCast(col)), y);
try std.testing.expectEqualStrings(&.{byte}, cell.grapheme());
- try std.testing.expectEqual(pardes.Color{ .index = 2 }, cell.style.fg);
+ try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().palette.?[2] }, cell.style.fg);
}
while (p.nextEffect()) |_| {}
p.update(.{ .key = .{ .cp = 'f', .text = "f" } });
@@ -1163,6 +1342,13 @@ const PdfTests = struct {
if (!enabled) try std.testing.expectEqual(@as(usize, 0), @sizeOf(State));
}
+ test "compact PDF saved prefix separates page status from editable commands" {
+ const path = "/tmp/a document.pdf";
+ try std.testing.expectEqualStrings(path ++ " [2/9]", legacySavedPrefix(path, path ++ " [2/9] Keep Del").?);
+ try std.testing.expect(legacySavedPrefix(path, path ++ " [oops] Keep Del") == null);
+ try std.testing.expect(legacySavedPrefix(path, path ++ " [2/9/4] Keep Del") == null);
+ }
+
test "PDF section rows preserve DFS ordinals and sanitise hierarchy" {
if (!enabled) return;
const entries = [_]pdf.OutlineEntry{
@@ -1296,6 +1482,9 @@ const PdfTests = struct {
};
const TerminalTests = struct {
+ // Tests asserting raw ANSI indices explicitly select helix, whose palette
+ // is inherited. Native-palette coverage lives in the overlay/eviction and
+ // trailing-blank tests and checks their concrete RGB values instead.
const gridOffset = panes.Terminal.gridOffset;
const scrollGrid = panes.Terminal.scrollGrid;
const promptInputReady = panes.Terminal.promptInputReady;
@@ -1536,7 +1725,7 @@ const TerminalTests = struct {
test "an edit buffer slides shell rows and their colors together" {
const testing = std.testing;
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 12 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 18, .rows = 12 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -1586,7 +1775,7 @@ const TerminalTests = struct {
test "a combining mark in the prompt keeps the command and its colors aligned" {
const testing = std.testing;
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 6 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 18, .rows = 6 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -1620,7 +1809,7 @@ const TerminalTests = struct {
test "colors are never taken from shell rows below the viewport" {
const testing = std.testing;
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 14 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 18, .rows = 14 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -1678,7 +1867,7 @@ const TerminalTests = struct {
test "a row the edit buffer only swallowed keeps its color" {
const testing = std.testing;
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 12 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 18, .rows = 12 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -1715,7 +1904,7 @@ const TerminalTests = struct {
test "an edit buffer reaching past the dumped rows colors nothing from row zero" {
const testing = std.testing;
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 12 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 18, .rows = 12 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -1791,7 +1980,7 @@ const TerminalTests = struct {
};
for (prompts) |prompt| {
for ([_]bool{ false, true }) |filter| {
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 24, .rows = 6 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 24, .rows = 6 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -1972,7 +2161,7 @@ const TerminalTests = struct {
// background, and wraps the glyph to the next row. A head OWNS its column,
// so skipping it the way a tail is skipped left the row's final column bare.
for ([_]pardes.Pane.Mode{ .tty, .normal }) |mode| {
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 16, .rows = 8 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 16, .rows = 8 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -2123,7 +2312,7 @@ const TerminalTests = struct {
test "an emptied edit buffer does not shift the colors below it" {
const testing = std.testing;
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 12 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 18, .rows = 12 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -2157,7 +2346,7 @@ const TerminalTests = struct {
test "an edit overlay never changes tty-mode ansi colors" {
const testing = std.testing;
- const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 6 });
+ const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 18, .rows = 6 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
@@ -2583,7 +2772,7 @@ test "Colors restores syntax after rendering disabled files and disabled edits"
seen += 1;
for ([_]*pardes.Cell{ first, second }) |cell| {
try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().kw }, cell.style.fg);
- try std.testing.expect(cell.style.bold);
+ try std.testing.expect(!cell.style.bold);
}
}
try std.testing.expectEqual(@as(usize, 2), seen);
@@ -2599,6 +2788,8 @@ test "growing a file pane colors every newly visible Zig keyword" {
const file = &pane.file.?;
gpa.free(file.path);
file.path = try gpa.dupe(u8, "/resize.zig");
+ p.update(.{ .command = "SyntaxBold" });
+ try std.testing.expect(p.settings.syntax_bold);
p.settings.colors = true;
p.presentation.enabled = false;
var frame = std.heap.ArenaAllocator.init(gpa);
@@ -2689,7 +2880,7 @@ test "Zig keywords keep every rendered byte colored across line-number widths" {
.{ line, view.wrap, view.horizontal, sample, cell.grapheme() },
);
try std.testing.expectEqual(expected, cell.style.fg);
- try std.testing.expect(cell.style.bold);
+ try std.testing.expect(!cell.style.bold);
}
}
}
@@ -2720,7 +2911,8 @@ test "terminal overlays preserve trailing blank styles without coloring inserted
const rect = p.rects[0];
const x = rect.x + config.GUTTER;
const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
- const colors = [_]pardes.Color{ .{ .index = 1 }, .{ .index = 2 }, .{ .index = 4 } };
+ const palette = p.theme().palette.?;
+ const colors = [_]pardes.Color{ .{ .rgb = palette[1] }, .{ .rgb = palette[2] }, .{ .rgb = palette[4] } };
const original = try p.render(frame.allocator());
for (colors, 0..) |color, row| {
const cell = original.at(x, y + 1 + @as(u16, @intCast(row)));