From 232bfbbc4371a1fe9df210874118b4450dce7f9d Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 00:30:46 -0300 Subject: Let a pane's tag take a row for each line it holds A tag that is a text can hold a newline, and the tagline, the body under it and the pointer all assumed one row. The pane's tag now takes a row per line (up to eight, and never the body's last row); the body starts below it, the rows a pointer counts start the body after the tag's lines, a click on any line puts the tag's cursor there, and a pixel shell gets a tag layer per line. A tag of one line draws exactly as before. Co-Authored-By: Claude Opus 5.5 --- test/output.zig | 4 +-- test/panes.zig | 80 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 12 deletions(-) (limited to 'test') diff --git a/test/output.zig b/test/output.zig index c245e5f5..400e9508 100644 --- a/test/output.zig +++ b/test/output.zig @@ -77,7 +77,7 @@ fn outputCell(p: *Pardes, surface: *pardes.Surface, id: usize, byte: usize) *par const line = modal.lineSlice(pane.file.?.content, 0); const rect = p.rects[id]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane) + @as(u16, @intCast(panes.File.displayWidth(line[0..byte]))); - const y = p.bodyTop(rect); + const y = p.bodyTop(pane, rect); return surface.at(x, y); } @@ -490,7 +490,7 @@ test "Mini syntax colors survive toggles themes and rendering without source acc const surface = try p.render(frame.allocator()); const rect = p.rects[id]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(mini) + @as(u16, @intCast(column)); - const y = p.bodyTop(rect) + @as(u16, @intCast(position.row)); + const y = p.bodyTop(mini, rect) + @as(u16, @intCast(position.row)); if (enabled) try std.testing.expectEqualDeep(pardes.Color{ .rgb = p.theme().kw }, surface.at(x, y).style.fg); try std.testing.expectEqual(colors, mini.file.?.mini.?.colors.ptr); try std.testing.expectEqual(@as(usize, 0), mini.file.?.highlights.len); diff --git a/test/panes.zig b/test/panes.zig index b9fa9a88..4804c497 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -168,6 +168,66 @@ const TagLayerTests = struct { } }; +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 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + p.presentation.enabled = false; + p.settings.tag_bottom = bottom; + p.update(.tick); + try std.testing.expectEqual(@as(u16, 1), pane.tag_rows); + const height = p.rects[0].h; + const body_rows = pane.rows; + // Insert mode in a tag is a body's: Enter is a new line. + p.update(.{ .key = .{ .cp = ':' } }); + p.update(.{ .key = .{ .cp = 'A' } }); + p.update(.{ .key = .{ .cp = Key.enter } }); + p.update(.{ .key = .{ .cp = 'S', .text = "Second" } }); + try std.testing.expect(std.mem.endsWith(u8, Pardes.curTail(pane), "\nSecond")); + try std.testing.expectEqual(@as(u16, 2), pane.tag_rows); + // The pane keeps its height; the body gives the tag its row. + try std.testing.expectEqual(height, p.rects[0].h); + try std.testing.expectEqual(body_rows - 1, pane.rows); + var frame = std.heap.ArenaAllocator.init(std.testing.allocator); + defer frame.deinit(); + const surface = try p.render(frame.allocator()); + const r = p.rects[0]; + const tag_y = p.tagTop(pane, r); + const x = r.x + pardes.TAG_TEXT_INSET; + try std.testing.expectEqualStrings("/", surface.at(x, tag_y).grapheme()); + for ("Second", 0..) |byte, i| try std.testing.expectEqual(byte, surface.at(x + @as(u16, @intCast(i)), tag_y + 1).text[0]); + try std.testing.expectEqual(pardes.FontRole.tagline, surface.at(x, tag_y + 1).style.font_role); + // the caret is on the second line, after what was typed + try std.testing.expectEqual(tag_y + 1, surface.cursor.?.y); + try std.testing.expectEqual(x + 6, surface.cursor.?.x); + // the body starts under (or over) the whole tag + const body_y = p.bodyTop(pane, r); + try std.testing.expectEqual(if (bottom) r.y else r.y + 2, body_y); + try std.testing.expectEqualStrings("b", surface.at(r.x + config.GUTTER + panes.File.gutterWidth(pane), body_y).grapheme()); + // a layer to a line, for a pixel shell + var lines: usize = 0; + for (p.surface.tagLayers()) |*layer| { + if (layer.cols == 0 or layer.kind != .pane or layer.id != 0) continue; + try std.testing.expectEqual(tag_y + layer.line, layer.viewport.y); + lines += 1; + } + try std.testing.expectEqual(@as(usize, 2), lines); + // a click on the second line puts the tag's cursor on it + p.update(.{ .key = .{ .cp = Key.escape } }); + p.update(.{ .key = .{ .cp = ':' } }); + ClickCursorTests.click(p, .left, x + 2, tag_y + 1); + try std.testing.expect(pane.focus == .tag); + try std.testing.expectEqual(@as(i32, 1), pane.tag.cur_row); + try std.testing.expectEqual(@as(i32, 2), pane.tag.cur_col); + // and the pane's text, a row to a screen row, has both of its lines + const text = try pardes.edit.paneText(p, pane); + try std.testing.expect(std.mem.startsWith(u8, modal.lineSlice(text, 0), "/test.txt")); + try std.testing.expectEqualStrings("Second", modal.lineSlice(text, 1)); + try std.testing.expectEqualStrings("body\n", pane.file.?.content); + } +} + const TagNavigationTests = struct { fn key(p: *Pardes, cp: u21) void { p.update(.{ .key = .{ .cp = cp } }); @@ -521,7 +581,7 @@ const TreeContextTests = struct { try std.testing.expectEqual(@as(i32, 59), pane.wrapAt(visible.row).line); const cursor = surface.cursor orelse return error.MissingLookCursor; const rect = p.rects[0]; - const y = p.bodyTop(rect); + const y = p.bodyTop(pane, rect); try std.testing.expectEqual(y + @as(u16, @intCast(visible.row)), cursor.y); } } @@ -543,7 +603,7 @@ const TreeContextTests = struct { try std.testing.expectEqual(@as(i32, 7), pane.wrapAt(pane.context_rows).line); const rect = p.rects[0]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); - const y = p.bodyTop(rect); + const y = p.bodyTop(pane, rect); ClickCursorTests.click(p, .left, x + 6, y); try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); try std.testing.expectEqual(@as(i32, 6), pane.body.cur_col); @@ -552,7 +612,7 @@ const TreeContextTests = struct { try std.testing.expectEqual(x + 6, cursor.x); // Read the body top again: the click cleared the announcement band the // builtin above put up, and the body moved back up with it. - try std.testing.expectEqual(p.bodyTop(p.rects[0]), cursor.y); + try std.testing.expectEqual(p.bodyTop(pane, p.rects[0]), cursor.y); p.update(.{ .key = .{ .cp = 'i' } }); p.update(.{ .key = .{ .cp = 'Z', .text = "Z" } }); try std.testing.expect(std.mem.startsWith(u8, pane.file.?.content, "const ZBox = struct {")); @@ -591,7 +651,7 @@ const TreeContextTests = struct { const surface = try p.render(frame.allocator()); try std.testing.expectEqual(@as(u16, 3), pane.context_rows); const rect = p.rects[0]; - const y = p.bodyTop(rect); + const y = p.bodyTop(pane, rect); var glyph_x: ?u16 = null; for (0..surface.cols) |col| { if (std.mem.eql(u8, surface.at(@intCast(col), y).grapheme(), "界")) glyph_x = @intCast(col); @@ -605,7 +665,7 @@ const TreeContextTests = struct { try std.testing.expectEqual(x, cursor.x); // Read the body top again: the click cleared the announcement band the // builtin above put up, so the body moved with it. - try std.testing.expectEqual(p.bodyTop(p.rects[0]), cursor.y); + try std.testing.expectEqual(p.bodyTop(pane, p.rects[0]), cursor.y); panes.File.setContent(p, &pane.file.?, try p.gpa.dupe(u8, "// no enclosing declaration\n" ** 18)); _ = try p.render(frame.allocator()); try std.testing.expectEqual(@as(u16, 0), pane.context_rows); @@ -664,7 +724,7 @@ const TtySelectionTests = struct { _ = try p.render(frame.allocator()); const rect = p.rects[0]; const x = rect.x + config.GUTTER; - const y = p.bodyTop(rect); + const y = p.bodyTop(pane, rect); JumpSelectionTests.drag(p, x + 1, y, x + 3, y); try std.testing.expectEqualStrings("lph", p.yank orelse return error.MissingAutomaticTtyYank); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); @@ -694,7 +754,7 @@ const TtySelectionTests = struct { _ = try p.render(frame.allocator()); const rect = p.rects[0]; const x = rect.x + config.GUTTER; - const y = p.bodyTop(rect); + const y = p.bodyTop(pane, rect); JumpSelectionTests.drag(p, x + @as(u16, if (prompt) 0 else 1), y, x + 3, y + @as(u16, if (prompt) 0 else 1)); const expected = if (prompt) "$ ab" else "lph\nrav"; try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); @@ -1009,7 +1069,7 @@ const JumpSelectionTests = struct { if (wrap) try std.testing.expectEqual(pane.wrap_line[0], pane.wrap_line[1]); const rect = p.rects[0]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); - const y = p.bodyTop(rect); + const y = p.bodyTop(pane, rect); if (reverse) drag(p, x + last_col, y + 1, x + first_col, y) else @@ -1040,7 +1100,7 @@ const JumpSelectionTests = struct { // A selection created after Back must also disappear on Forward. // Back announces itself, so the body sits under a notice band now. - const y2 = p.bodyTop(p.rects[0]); + const y2 = p.bodyTop(pane, p.rects[0]); drag(p, x + 1, y2, x + 3, y2 + 1); try std.testing.expect(pardes.test_api.heldSelection(p, 0) != null); p.update(.{ .key = .{ .cp = 'i', .ctrl = true } }); @@ -4093,7 +4153,7 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu .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 body_y = p.bodyTop(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 p.tagPrefix(pane)))) + 3 else -- cgit v1.3