diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-11 12:38:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 682e237df7e8b22f820d14a4adee58e6e2f84268 (patch) | |
| tree | 5d888cb46f1a49e89fc55e2a4c232d3af0265b1d /test/panes.zig | |
| parent | 6878e1c309172d624c0d7a3555f6f0217ae6770e (diff) | |
| download | pardes-682e237df7e8b22f820d14a4adee58e6e2f84268.tar.gz pardes-682e237df7e8b22f820d14a4adee58e6e2f84268.zip | |
trunk: resume before the Reload experiment
Empty marker on the last pre-Reload change. Keep the Reload experiment on reload (3801914), its first change on reload-start (200a1fc), and the unfinished performance investigation on reload-perf-wip.
Diffstat (limited to 'test/panes.zig')
| -rw-r--r-- | test/panes.zig | 220 |
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))); |
