diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 19:36:58 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 1df6bd12fafbf97062f7844a23de208add75a843 (patch) | |
| tree | 78b61cfebeb12c40b679d053b457d2e28f232784 /test/panes.zig | |
| parent | 1f8de151e382755c1ce597f166ea4552e7f7e8d1 (diff) | |
| download | pardes-1df6bd12fafbf97062f7844a23de208add75a843.tar.gz pardes-1df6bd12fafbf97062f7844a23de208add75a843.zip | |
A pane tag wraps onto rows and can collapse to one, as acme's does
acme's frame wraps a tag at its right edge and the window grows to show
every row (wind.c wintaglines); Tagup/Tagdown shrink it to one row and back.
Pane tags used to be one row, scrolled sideways to the caret. Now a tag's
lines wrap at its width (one cell kept for the caret past a full row) and it
starts expanded; insert-mode Up on the first row collapses it, Down on the
last row expands it, and Alt-Up/Alt-Down do so in either mode. A word the
wrap breaks across rows is still one word to a click and a selection.
Column and workspace tags keep scrolling sideways.
Tests read body rows relative to the body's first row; the 41 snapshot
goldens that moved were re-recorded by name after a programmatic check that
each differs only by body rows shifting under the taller tags, and twelve
scripts whose clicks targeted fixed rows now click the same text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'test/panes.zig')
| -rw-r--r-- | test/panes.zig | 207 |
1 files changed, 136 insertions, 71 deletions
diff --git a/test/panes.zig b/test/panes.zig index a6a95124..836323a0 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -165,6 +165,23 @@ const TagLayerTests = struct { } }; + +// Rows relative to where the pane's body and tag are, whatever height its +// tag has (a tag wraps onto more rows when it is long). +fn paneAt(p: *Pardes, r: anytype) *pardes.Pane { + for (p.panes, 0..) |slot, id| if (slot) |pane| if (std.meta.eql(p.rects[id], r)) return pane; + unreachable; +} +fn bodyY(p: *Pardes, r: anytype) u16 { + return p.bodyTop(paneAt(p, r), r); +} +fn bodyH(p: *Pardes, r: anytype) u16 { + return r.h -| paneAt(p, r).tag_rows; +} +fn tagY(p: *Pardes, r: anytype) u16 { + return p.tagTop(paneAt(p, r), r); +} + test "a tag with a newline in it is as tall as its lines, and each line is typed into and pointed at" { for ([_]bool{ false, true }) |bottom| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 14 }); @@ -407,7 +424,7 @@ const TagNavigationTests = struct { 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; + const y = tagY(p, rect); const prefix: u16 = @intCast(panes.File.displayWidth(try pardes.tagline.tagPrefix(p, first))); ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + prefix + 6, y); try std.testing.expectEqual(@as(usize, 0), p.active); @@ -774,6 +791,8 @@ const TreeContextTests = struct { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 48, .rows = 14 }); defer p.deinit(); const pane = try setZigFile(p, "pub fn @\"界name\"(\n\tvalue: usize,\n) void {\n" ++ " _ = value;\n" ** 12 ++ "}\n"); + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); p.settings.wrap = false; p.settings.colors = false; try std.testing.expect(p.executeBuiltinLine(0, "TreeContext")); @@ -811,6 +830,8 @@ const TreeContextTests = struct { defer p.deinit(); const body = " // 012345678901234567890123456789界tail"; const pane = try setZigFile(p, "pub fn aLongDeclarationNameThatCannotFit() void {\n // before\n" ++ body ++ "\n}\n"); + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); p.settings.wrap = true; try std.testing.expect(p.executeBuiltinLine(0, "TreeContext")); pane.scrollBy(2); @@ -953,14 +974,14 @@ const TtySelectionTests = struct { _ = try p.render(frame.allocator()); const source = p.rects[0]; const sx = source.x + config.GUTTER; - const sy = if (p.settings.tag_bottom) source.y else source.y + pardes.BOX_H; + const sy = bodyY(p, source); JumpSelectionTests.drag(p, sx + 1, sy, sx + 3, sy + 1); try std.testing.expectEqualStrings("lpha\nbrav", p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank); try noChildInput(p); const target = p.rects[1]; const tx = target.x + config.GUTTER + 2; - const ty = if (p.settings.tag_bottom) target.y else target.y + pardes.BOX_H; + const ty = bodyY(p, target); p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = tx, .row = ty } }); p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = tx, .row = ty } }); p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = tx, .row = ty } }); @@ -1180,7 +1201,7 @@ const ClickCursorTests = struct { _ = try p.render(frame.allocator()); const rect = p.rects[0]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); - const y = if (bottom_tag) rect.y else rect.y + pardes.BOX_H; + const y = bodyY(p, rect); click(p, .left, x + 15, y + case.click_row); try std.testing.expectEqual(case.row, pane.body.cur_row); try std.testing.expectEqual(case.col, pane.body.cur_col); @@ -1249,7 +1270,7 @@ const ClickCursorTests = struct { _ = try p.render(frame.allocator()); const rect = p.rects[0]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); - const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const y = bodyY(p, rect); JumpSelectionTests.drag(p, x + 1, y + 4, x + 3, y + 4); try std.testing.expectEqualStrings("lph", pardes.test_api.heldSelection(p, 0).?); for (0..2) |_| p.update(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = x + 2, .row = y + 4 } }); @@ -1458,22 +1479,29 @@ const TagNameTintTests = struct { var arena = std.heap.ArenaAllocator.init(p.gpa); defer arena.deinit(); const surface = try p.render(arena.allocator()); - const text = try pardes.tagline.tagText(p, arena.allocator(), pane); + // The tag as shown: its rows wrapped, each offset mapped back to the + // tag's own to say whether it is in the name. + const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane); const base = std.mem.lastIndexOfScalar(u8, name, '/') orelse return error.MissingDirectory; const rect = p.rects[id]; - const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; const left = rect.x + pardes.TAG_TEXT_INSET; var visible_name: usize = 0; - var at: usize = 0; - while (at < text.len) : (at = modal.nextGrapheme(text, at)) { - const start: i32 = @as(i32, @intCast(panes.File.rawDisplayCol(text, at))) - pane.tag_scroll; - const end = start + @as(i32, @intCast(panes.File.displayWidth(text[at..modal.nextGrapheme(text, at)]))); - var col = @max(0, start); - while (col < end and col < rect.w - pardes.TAG_TEXT_INSET) : (col += 1) { - const in_name = at > base and at < name.len; - const fg = if (in_name) name_fg else tag_fg; - try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(left + @as(u16, @intCast(col)), y).style.fg); - if (in_name) visible_name += 1; + for (0..pane.tag_rows) |row| { + const line_start = modal.lineStartOffset(shown.text, row + pane.tag_top); + const line = modal.lineSlice(shown.text, row + pane.tag_top); + const y = tagY(p, rect) + @as(u16, @intCast(row)); + var at: usize = 0; + while (at < line.len) : (at = modal.nextGrapheme(line, at)) { + const real = shown.toReal(line_start + at); + const start: i32 = @intCast(panes.File.rawDisplayCol(line, at)); + const end = start + @as(i32, @intCast(panes.File.displayWidth(line[at..modal.nextGrapheme(line, at)]))); + var col = start; + while (col < end and col < rect.w - pardes.TAG_TEXT_INSET) : (col += 1) { + const in_name = real > base and real < name.len; + const fg = if (in_name) name_fg else tag_fg; + try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(left + @as(u16, @intCast(col)), y).style.fg); + if (in_name) visible_name += 1; + } } } return visible_name; @@ -1499,7 +1527,7 @@ const TagNameTintTests = struct { } } - test "tag filename tint follows scrolled Unicode drafted names and yields to selection" { + test "tag filename tint follows wrapped 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"); @@ -1509,17 +1537,20 @@ const TagNameTintTests = struct { pardes.tagline.draftName(p, pane, 0); redraft(pane, name); pane.input.cur_col = @intCast(std.mem.indexOf(u8, name, "界").?); + p.sync(); // the drafted name's rows try std.testing.expect((try check(p, 0, name)) > 0); - try std.testing.expect(pane.tag_scroll > 0); - // 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 + try std.testing.expect(pane.tag_rows > 1); // the long name wraps + // A sweep over the wide glyph, on the row it wrapped to, wears the + // selection over the tint. var arena = std.heap.ArenaAllocator.init(p.gpa); defer arena.deinit(); + const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane); + const pos = modal.positionAt(shown.text, shown.fromReal(@intCast(pane.input.cur_col))); + const col: i32 = @intCast(panes.File.rawDisplayCol(modal.lineSlice(shown.text, pos.row), pos.col)); + pane.sel[0] = .{ .state = .done, .c0 = col, .c1 = col + 1, .r0 = @intCast(pos.row), .r1 = @intCast(pos.row) }; const surface = try p.render(arena.allocator()); - 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; + const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col)); + const y = tagY(p, p.rects[0]) + @as(u16, @intCast(pos.row)); 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); } @@ -1542,7 +1573,7 @@ const TagNameTintTests = struct { p.settings.focus_tint = active; const surface = try p.render(arena.allocator()); const x = p.rects[0].x + pardes.TAG_TEXT_INSET; - const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y; + const y = tagY(p, p.rects[0]); const fg = if (active) active_fg else tag_fg; try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 1, y).style.fg); try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 5, y).style.fg); @@ -1561,7 +1592,7 @@ fn redraft(pane: *Pane, name: []const u8) void { /// 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 y = tagY(p, rect); 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); @@ -1703,7 +1734,7 @@ test "paste into pane commands and prompts never inserts into the body" { try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), " Custom\nCommand") != null); } -test "long pane tags reveal commands and map visible clicks to whole tag addresses" { +test "a long pane tag wraps onto rows, and a click on a wrapped row maps to the whole tag's address" { 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"); @@ -1715,13 +1746,15 @@ test "long pane tags reveal commands and map visible clicks to whole tag address 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(pane.tag_rows > 1); try std.testing.expect(surface.cursor.?.x < p.rects[0].x + p.rects[0].w); const text = try pardes.tagline.tagText(p, p.gpa, pane); defer p.gpa.free(text); - const offset = std.mem.indexOf(u8, text, "SyntaxBold").?; - const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + panes.File.rawDisplayCol(text, offset) - pane.tag_scroll); - const y = p.rects[0].y; + const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane); + const at = shown.fromReal(std.mem.indexOf(u8, text, "SyntaxBold").?); + const pos = modal.positionAt(shown.text, at); + const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + panes.File.rawDisplayCol(modal.lineSlice(shown.text, pos.row), pos.col)); + const y = p.tagTop(pane, p.rects[0]) + @as(u16, @intCast(pos.row)); 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 } }); @@ -1729,6 +1762,32 @@ test "long pane tags reveal commands and map visible clicks to whole tag address try std.testing.expectEqualStrings(text, try pardes.tagline.tagText(p, p.scratch.allocator(), pane)); } +test "a word the tag's wrapping breaks across rows is still one word to a click" { + 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"); + try pane.appendTag(" SyntaxBold"); + var arena = std.heap.ArenaAllocator.init(p.gpa); + defer arena.deinit(); + // Lengthen the path until the wrap splits the word after "Synt". + var pad: usize = 1; + const shown = while (pad < 80) : (pad += 1) { + p.gpa.free(pane.file.?.path); + const path = try std.fmt.allocPrint(arena.allocator(), "/{s}/f.zig", .{("a" ** 80)[0..pad]}); + pane.file.?.path = try p.gpa.dupe(u8, path); + p.sync(); + const s = try pardes.tagline.shownTag(p, arena.allocator(), pane); + if (std.mem.indexOf(u8, s.text, "Synt\n") != null) break s; + } else return error.NoSplit; + const pos = modal.positionAt(shown.text, std.mem.indexOf(u8, shown.text, "Synt").?); + const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + pos.col + 1); + const y = p.tagTop(pane, p.rects[0]) + @as(u16, @intCast(pos.row)); + 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); +} + 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 }); @@ -1751,7 +1810,7 @@ test "terminal overlay recoloring never materializes unrelated history" { const surface = try p.render(frame.allocator()); try testing.expect(p.shell_rows.pane == null); const rect = p.rects[0]; - const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const body_y = bodyY(p, rect); const cell = surface.at(rect.x + config.GUTTER, body_y); try testing.expectEqualStrings("c", cell.grapheme()); try testing.expectEqual(pardes.Color{ .rgb = p.theme().palette.?[2] }, cell.style.fg); @@ -1799,7 +1858,7 @@ test "terminal output keeps following new rows after scrollback eviction" { const surface = try p.render(frame.allocator()); 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) + pane.rows - 2; + const y = (bodyY(p, rect)) + pane.rows - 2; for (last, 0..) |byte, col| { const cell = surface.at(x + @as(u16, @intCast(col)), y); try std.testing.expectEqualStrings(&.{byte}, cell.grapheme()); @@ -2452,7 +2511,7 @@ test "unplaced terminal panes keep their allocated grid until placement" { p.update(.tick); try std.testing.expect(p.rects[id].w > 0 and p.rects[id].h > pardes.BOX_H); try std.testing.expectEqual(p.rects[id].w - config.GUTTER, pane.cols); - try std.testing.expectEqual(p.rects[id].h - pardes.BOX_H, pane.rows); + try std.testing.expectEqual(bodyH(p, p.rects[id]), pane.rows); } test "document panes ignore terminal output without allocating terminal state" { @@ -2805,7 +2864,7 @@ const OutputTests = struct { layout.compute(p); const surface = try p.render(frame.allocator()); const rect = p.rects[pane.search_pane.?]; - const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const body_y = bodyY(p, rect); for (file.location_rows, 0..) |row, index| { if (!row.declaration) continue; const line = modal.lineSlice(file.content, index); @@ -2853,7 +2912,7 @@ const OutputTests = struct { _ = try p.render(frame.allocator()); const rect = p.rects[id]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(help) + @as(u16, @intCast(at - line_start + 2)); - const y = (if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H) + @as(u16, @intCast(row)); + const y = (bodyY(p, rect)) + @as(u16, @intCast(row)); p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .press, .col = x, .row = y } }); p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .release, .col = x, .row = y } }); try std.testing.expect(p.active != id); @@ -2923,7 +2982,7 @@ const OutputTests = struct { p.acknowledgePanelPresentation(&.{}); const rect = p.rects[id]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(result); - const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const y = bodyY(p, rect); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = x, .row = y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .drag, .col = x + 2, .row = y + 1 } }); try std.testing.expect(p.drag == .select); @@ -3238,8 +3297,8 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const rows: usize = r.h - pardes.BOX_H; + const body_y = bodyY(p, r); + const rows: usize = bodyH(p, r); const cols: usize = r.w -| config.GUTTER; const Snap = struct { text: [][7]u8, len: []u8, style: []pardes.CellStyle }; @@ -3479,7 +3538,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const red: pardes.Color = .{ .index = 1 }; const green: pardes.Color = .{ .index = 2 }; const blue: pardes.Color = .{ .index = 4 }; @@ -3534,7 +3593,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3582,8 +3641,8 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const body_h = r.h - pardes.BOX_H; + const body_y = bodyY(p, r); + const body_h = bodyH(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3621,7 +3680,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const red: pardes.Color = .{ .index = 1 }; const green: pardes.Color = .{ .index = 2 }; const blue: pardes.Color = .{ .index = 4 }; @@ -3658,7 +3717,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const red: pardes.Color = .{ .index = 1 }; // Covers far more rows than the grid was ever dumped for, so the anchor @@ -3740,7 +3799,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3818,8 +3877,8 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const rows: usize = r.h - pardes.BOX_H; + const body_y = bodyY(p, r); + const rows: usize = bodyH(p, r); const cols: usize = r.w -| config.GUTTER; // What tty mode shows with nothing left behind: the reference. @@ -3870,6 +3929,8 @@ const TerminalTests = struct { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); pane.tty_filter = filter; pane.body.mode = mode; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[32m\u{754C}\x1b[41m\x1b[K\x1b[7G\x1b[44m\x1b[K\r\n" } }); @@ -3878,7 +3939,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3914,7 +3975,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3947,10 +4008,10 @@ const TerminalTests = struct { const s = try p.render(frame.allocator()); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); var bad: usize = 0; - for (0..r.h -| pardes.BOX_H) |vr| { + for (0..bodyH(p, r)) |vr| { var buf: [16]u8 = undefined; var n: usize = 0; for (0..10) |c| { @@ -3990,7 +4051,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.update(.{ .output = .{ .pane = 0, .bytes = "plain text\r\n" } }); p.shell_rows.stale = true; @@ -4028,7 +4089,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); // The state two ordinary edits reach: one at the bottom, one that split a // line further up. The buffer now spans rows 2..6 and diverges at BOTH @@ -4066,7 +4127,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); // The state three keystrokes reach on any blank shell row: type a character // and delete it, and the buffer holds NO text while still standing in for @@ -4091,6 +4152,8 @@ const TerminalTests = struct { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); pane.tty_filter = false; pane.body.mode = .tty; @@ -4100,7 +4163,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const blue: pardes.Color = .{ .index = 4 }; p.shell_rows.stale = true; @@ -4143,12 +4206,12 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const s = try p.render(frame.allocator()); const want = pardes.Color{ .index = 33 }; var seen = false; - for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| { + for (0..@as(usize, bodyH(p, r))) |vr| { var buf: [15]u8 = undefined; for (0..15) |c| { const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme(); @@ -4196,11 +4259,11 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const s = try p.render(frame.allocator()); var seen = false; - for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| { + for (0..@as(usize, bodyH(p, r))) |vr| { var buf: [12]u8 = undefined; for (0..12) |c| { const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme(); @@ -4250,8 +4313,8 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const body_h = r.h -| pardes.BOX_H; + const body_y = bodyY(p, r); + const body_h = bodyH(p, r); var step: usize = 0; while (step < 12) : (step += 1) { @@ -4345,11 +4408,11 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const s = try p.render(frame.allocator()); var seen: usize = 0; - for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| { + for (0..@as(usize, bodyH(p, r))) |vr| { var buf: [12]u8 = undefined; for (0..12) |c| { const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme(); @@ -4427,7 +4490,7 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu switch (gesture) { .body, .tag => { const rect = p.rects[0]; - const tag_y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; + const tag_y = tagY(p, rect); const body_y = p.bodyTop(pane, rect); const x = if (gesture == .tag) rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(try pardes.tagline.tagPrefix(p, pane)))) + 3 @@ -4557,9 +4620,9 @@ test "growing a file pane colors every newly visible Zig keyword" { _ = frame.reset(.retain_capacity); const surface = try p.render(frame.allocator()); const rect = p.rects[0]; - const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const body_y = bodyY(p, rect); var seen: usize = 0; - for (body_y..body_y + rect.h - pardes.BOX_H) |y| { + for (body_y..body_y + bodyH(p, rect)) |y| { for (rect.x + config.GUTTER..rect.x + rect.w - 1) |x| { const first = surface.at(@intCast(x), @intCast(y)); const second = surface.at(@intCast(x + 1), @intCast(y)); @@ -4600,6 +4663,8 @@ test "Zig keywords keep every rendered byte colored across line-number widths" { const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = view.cols, .rows = 10 }); defer p.deinit(); const pane = try p.setTestFile(source); + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); const file = &pane.file.?; gpa.free(file.path); file.path = try gpa.dupe(u8, "/highlight.zig"); @@ -4616,8 +4681,8 @@ test "Zig keywords keep every rendered byte colored across line-number widths" { var seen: usize = 0; var if_position: ?struct { x: u16, y: u16 } = null; const rect = p.rects[0]; - const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; - for (body_y..body_y + rect.h - pardes.BOX_H) |y| { + const body_y = bodyY(p, rect); + for (body_y..body_y + bodyH(p, rect)) |y| { for (rect.x + config.GUTTER..rect.x + rect.w - 1) |x| { const first = surface.at(@intCast(x), @intCast(y)); const second = surface.at(@intCast(x + 1), @intCast(y)); @@ -4666,7 +4731,7 @@ test "terminal overlays preserve trailing blank styles without coloring inserted defer frame.deinit(); 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 y = bodyY(p, rect); 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()); |
