From 9d2e07e997de155c7dbb894a79e5c11839eab39e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 01:01:35 -0300 Subject: Scroll a tag taller than its rows to keep its caret in view A tag shows at most eight rows, so a longer one (a 9P write of many lines, or `o` in the tag) put its caret on a line that was never drawn. Pane, column and workspace tags now keep a first line on screen that follows the caret while they have the keyboard, and the pointer and the pane text count their rows from it. Co-Authored-By: Claude Opus 5.5 --- test/panes.zig | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) (limited to 'test/panes.zig') diff --git a/test/panes.zig b/test/panes.zig index 954e431b..9b800430 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -228,6 +228,66 @@ test "a tag with a newline in it is as tall as its lines, and each line is typed } } +test "a tag with more lines than rows scrolls to keep its caret in view" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 30 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + p.presentation.enabled = false; + var many: std.ArrayList(u8) = .empty; + defer many.deinit(std.testing.allocator); + for (1..21) |n| try many.print(std.testing.allocator, "\nL{d}", .{n}); + try pane.appendTag(many.items); + p.update(.tick); + try std.testing.expectEqual(@as(u16, pardes.MAX_TAG_ROWS), pane.tag_rows); + var frame = std.heap.ArenaAllocator.init(std.testing.allocator); + defer frame.deinit(); + const r = p.rects[0]; + const x = r.x + pardes.TAG_TEXT_INSET; + // To the last line: the tag scrolls so it is the bottom row, caret on it. + p.update(.{ .key = .{ .cp = ':' } }); + p.update(.{ .key = .{ .cp = 'g' } }); + p.update(.{ .key = .{ .cp = 'e' } }); + try std.testing.expectEqual(@as(i32, 20), pane.tag.cur_row); + const bottom = try p.render(frame.allocator()); + const tag_y = p.tagTop(pane, r); + const last_row = tag_y + pardes.MAX_TAG_ROWS - 1; + try std.testing.expectEqual(last_row, bottom.cursor.?.y); + try std.testing.expectEqualStrings("L", bottom.at(x, last_row).grapheme()); + try std.testing.expectEqualStrings("2", bottom.at(x + 1, last_row).grapheme()); + try std.testing.expectEqualStrings("0", bottom.at(x + 2, last_row).grapheme()); + // what the pointer sees is what is shown: its first row is line 13 + const text = try pardes.edit.paneText(p, pane); + try std.testing.expectEqualStrings("L13", modal.lineSlice(text, 0)); + ClickCursorTests.click(p, .left, x + 1, tag_y + 1); + try std.testing.expectEqual(@as(i32, 14), pane.tag.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.tag.cur_col); + // Back to the first line brings the prefix back into view. + p.update(.{ .key = .{ .cp = Key.escape } }); + p.update(.{ .key = .{ .cp = 'g' } }); + p.update(.{ .key = .{ .cp = 'g' } }); + const top = try p.render(frame.allocator()); + try std.testing.expectEqual(@as(u16, 0), pane.tag_top); + try std.testing.expectEqual(tag_y, top.cursor.?.y); + try std.testing.expectEqualStrings("/", top.at(x, tag_y).grapheme()); + + // A column tag does the same in its rows. + many.clearRetainingCapacity(); + try many.appendSlice(std.testing.allocator, "New"); + for (1..21) |n| try many.print(std.testing.allocator, "\nC{d}", .{n}); + p.col_tags[0].own = try p.gpa.dupe(u8, many.items); + p.update(.tick); + const rows = p.columnBarHeight(); + try std.testing.expectEqual(@as(u16, pardes.MAX_TAG_ROWS), rows); + pardes.tagline.enterHeader(p, 0); + p.update(.{ .key = .{ .cp = 'g' } }); + p.update(.{ .key = .{ .cp = 'e' } }); + const header = try p.render(frame.allocator()); + const header_row = p.topBarHeight() + rows - 1; + try std.testing.expectEqual(header_row, header.cursor.?.y); + try std.testing.expectEqualStrings("C", header.at(p.col_x[0] + pardes.TAG_TEXT_INSET, header_row).grapheme()); + try std.testing.expectEqualStrings("2", header.at(p.col_x[0] + pardes.TAG_TEXT_INSET + 1, header_row).grapheme()); +} + const TagNavigationTests = struct { fn key(p: *Pardes, cp: u21) void { p.update(.{ .key = .{ .cp = cp } }); -- cgit v1.3