summaryrefslogtreecommitdiff
path: root/test/panes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'test/panes.zig')
-rw-r--r--test/panes.zig385
1 files changed, 291 insertions, 94 deletions
diff --git a/test/panes.zig b/test/panes.zig
index 40b4b6ba..f00b35c4 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -86,6 +86,8 @@ const TagLayerTests = struct {
const source_y = @as(f32, @floatFromInt(layer.viewport.y)) + 0.5;
const painted_x = shown.x + (x / 10 - track.to.x) / track.to.w * shown.w;
const painted_y = shown.y + (source_y - track.to.y) / track.to.h * shown.h;
+ // Each is a first click, whatever cell the one before hit.
+ p.last_click = null;
for ([_]pardes.Mouse.Kind{ .press, .release }) |kind| {
var event = mouse(layer, x, kind);
// Hosts inverse the exact painted pixel into the tag envelope;
@@ -103,7 +105,6 @@ const TagLayerTests = struct {
defer p.deinit();
_ = try p.setTestFile("body\n");
p.presentation.enabled = false;
- p.settings.column_tags = true;
p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 };
try std.testing.expect(p.executeBuiltinLine(0, "New"));
const second = p.active;
@@ -163,6 +164,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 });
@@ -267,22 +285,14 @@ test "a tag with more lines than rows scrolls to keep its caret in view" {
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.
+ // A column tag is one row, as acme's: its lines are joined.
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());
+ try std.testing.expectEqual(pardes.COLBAR_H, p.columnBarHeight());
+ try std.testing.expect(std.mem.indexOfScalar(u8, p.col_tags[0].own.?, '\n') == null);
}
const TagNavigationTests = struct {
@@ -413,7 +423,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);
@@ -595,7 +605,8 @@ const BodyHitTests = struct {
p.update(.{ .mouse = mouse(layer, last - 1, prefix + 11, .press) });
// Release at a different source row without an intermediate drag event.
p.update(.{ .mouse = mouse(layer, last, prefix + 14, .release) });
- try std.testing.expectEqualStrings("body\nbody", pardes.test_api.heldSelection(p, 0).?);
+ // A stream: the last row from its start, indentation and all.
+ try std.testing.expect(std.mem.endsWith(u8, pardes.test_api.heldSelection(p, 0).?, "// body") and std.mem.startsWith(u8, pardes.test_api.heldSelection(p, 0).?, "body\n"));
p.update(.{ .mouse = mouse(layer, last, 10, .press) });
p.update(.{ .mouse = mouse(layer, last, 10, .release) });
try std.testing.expectEqual(expected, pane.body.cur_row);
@@ -779,6 +790,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"));
@@ -816,6 +829,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);
@@ -873,7 +888,7 @@ const TtySelectionTests = struct {
try noChildInput(p);
}
- test "tty selection preserves exact rectangle through yank clipboard and mode changes" {
+ test "tty selection preserves its exact stream through yank clipboard and mode changes" {
if (comptime !panes.Terminal.enabled) return error.SkipZigTest;
for ([_]bool{ false, true }) |prompt| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 });
@@ -894,7 +909,7 @@ const TtySelectionTests = struct {
const x = rect.x + config.GUTTER;
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";
+ const expected = if (prompt) "$ ab" else "lpha\nbrav";
try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode);
try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?);
try std.testing.expectEqualStrings(expected, p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank);
@@ -907,9 +922,13 @@ const TtySelectionTests = struct {
try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode);
const normal = try p.render(frame.allocator());
if (!prompt) {
- const selection_bg: pardes.Color = .{ .rgb = p.theme().sel_bg };
- try std.testing.expect(!std.meta.eql(selection_bg, normal.at(x + 4, y).style.bg));
- try std.testing.expect(!std.meta.eql(selection_bg, normal.at(x, y + 1).style.bg));
+ // A stream: the first row to its edge, the last from its
+ // start; (x + 1, y) is surely in it, whatever its tint.
+ const selected = normal.at(x + 1, y).style.bg;
+ try std.testing.expect(std.meta.eql(selected, normal.at(x + 4, y).style.bg));
+ try std.testing.expect(std.meta.eql(selected, normal.at(x, y + 1).style.bg));
+ try std.testing.expect(!std.meta.eql(selected, normal.at(x + 4, y + 1).style.bg));
+ try std.testing.expect(!std.meta.eql(selected, normal.at(x, y).style.bg));
}
try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?);
p.update(.{ .key = .{ .cp = 'y' } });
@@ -954,14 +973,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("lph\nrav", p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank);
+ 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 } });
@@ -976,13 +995,13 @@ const TtySelectionTests = struct {
}
const expected = try std.fmt.allocPrint(p.gpa, "{s}{s}", .{
if (reports_mouse) "\x1b[<0;3;1M\x1b[<0;3;1m" else "",
- if (bracketed) "\x1b[200~lph\nrav\x1b[201~" else "lph\rrav",
+ if (bracketed) "\x1b[200~lpha\nbrav\x1b[201~" else "lpha\rbrav",
});
defer p.gpa.free(expected);
try std.testing.expectEqualStrings(expected, written.written());
try std.testing.expectEqual(panes.Text.Mode.tty, p.panes[0].?.body.mode);
try std.testing.expectEqual(panes.Text.Mode.tty, p.panes[1].?.body.mode);
- try std.testing.expectEqualStrings("lph\nrav", pardes.test_api.heldSelection(p, 0).?);
+ try std.testing.expectEqualStrings("lpha\nbrav", pardes.test_api.heldSelection(p, 0).?);
}
}
}
@@ -1060,6 +1079,107 @@ const ClickCursorTests = struct {
p.update(.{ .mouse = .{ .button = button, .kind = .release, .col = x, .row = y } });
}
+ test "a B1 sweep held past a pane's bottom scrolls the body and keeps its start" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 });
+ defer p.deinit();
+ var text: std.ArrayList(u8) = .empty;
+ defer text.deinit(std.testing.allocator);
+ for (0..60) |i| try text.print(std.testing.allocator, "line{d}\n", .{i});
+ const pane = try p.setTestFile(text.items);
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect);
+ // The pane reaches the screen's bottom: its last row is the edge.
+ const below = rect.y + rect.h - 1;
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = x, .row = y + 1 } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = x + 2, .row = below } });
+ for (0..20) |_| p.update(.tick);
+ try std.testing.expect(pane.scroll() >= 20);
+ _ = try p.render(frame.allocator()); // a frame between the ticks and the release
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = x + 2, .row = below } });
+ const got = pardes.edit.currentSelText(p, pane).?;
+ try std.testing.expect(std.mem.startsWith(u8, got, "line1\nline2\n"));
+ try std.testing.expect(std.mem.count(u8, got, "\n") > 20);
+ }
+
+ test "everything a B1 chord does while B1 is down is one undo step" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("one two three\n");
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect);
+ const M = struct {
+ fn ev(pp: *Pardes, b: pardes.Mouse.Button, k: pardes.Mouse.Kind, cx: u16, cy: u16) void {
+ pp.update(.{ .mouse = .{ .button = b, .kind = k, .col = cx, .row = cy } });
+ }
+ };
+ // B1-B2 then B1-B3: cut, then paste back: a copy, one step.
+ const before = pane.file.?.history.undo_len;
+ M.ev(p, .left, .press, x + 4, y);
+ M.ev(p, .left, .drag, x + 6, y);
+ M.ev(p, .middle, .press, x + 6, y);
+ M.ev(p, .middle, .release, x + 6, y);
+ M.ev(p, .right, .press, x + 6, y);
+ M.ev(p, .right, .release, x + 6, y);
+ M.ev(p, .left, .release, x + 6, y);
+ try std.testing.expectEqualStrings("one two three\n", pane.file.?.content);
+ try std.testing.expectEqualStrings("two", p.registers.text(p.gpa, '"').?);
+ try std.testing.expect(pane.file.?.history.undo_len <= before + 1);
+ // A cut alone comes back with one Undo.
+ M.ev(p, .left, .press, x + 4, y);
+ M.ev(p, .left, .drag, x + 6, y);
+ M.ev(p, .middle, .press, x + 6, y);
+ M.ev(p, .middle, .release, x + 6, y);
+ M.ev(p, .left, .release, x + 6, y);
+ try std.testing.expectEqualStrings("one three\n", pane.file.?.content);
+ try std.testing.expect(p.executeBuiltinLine(0, "Undo"));
+ try std.testing.expectEqualStrings("one two three\n", pane.file.?.content);
+ }
+
+ test "a double-click selects a word, a line, a bracketed or quoted text, as acme's" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("call foo_bar(a, b)\nsay \"hi there\"\n");
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect);
+ const Want = struct { col: u16, row: u16, text: []const u8 };
+ for ([_]Want{
+ .{ .col = 6, .row = 0, .text = "foo_bar" },
+ .{ .col = 13, .row = 0, .text = "a, b" },
+ .{ .col = 0, .row = 0, .text = "call foo_bar(a, b)\n" },
+ .{ .col = 5, .row = 1, .text = "hi there" },
+ }) |w| {
+ click(p, .left, x + w.col, y + w.row);
+ click(p, .left, x + w.col, y + w.row);
+ try std.testing.expectEqualStrings(w.text, pardes.edit.currentSelText(p, pane).?);
+ // A third click is a plain click again: the caret, no selection.
+ click(p, .left, x + w.col, y + w.row);
+ try std.testing.expect(!pane.body.vsel.active);
+ }
+ // In the tag too.
+ const tag_text = try pardes.tagline.tagText(p, frame.allocator(), pane);
+ const del: u16 = @intCast(std.mem.indexOf(u8, tag_text, "Del").? + 1);
+ const tag_x = rect.x + pardes.TAG_TEXT_INSET + del;
+ click(p, .left, tag_x, p.tagTop(pane, rect));
+ click(p, .left, tag_x, p.tagTop(pane, rect));
+ try std.testing.expect(pane.tag.vsel.active);
+ const tag_now = try pardes.tagline.tagText(p, frame.allocator(), pane);
+ const a = modal.offsetAt(tag_now, .{ .row = @intCast(pane.tag.vsel.row), .col = @intCast(pane.tag.vsel.col) });
+ const h = modal.offsetAt(tag_now, .{ .row = @intCast(pane.tag.cur_row), .col = @intCast(pane.tag.cur_col) });
+ try std.testing.expectEqualStrings("Del", tag_now[a .. h + 1]);
+ }
+
test "click cursor and insertion agree beyond line and file ends" {
for ([_]bool{ false, true }) |already_insert| {
for ([_]bool{ false, true }) |bottom_tag| {
@@ -1080,7 +1200,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);
@@ -1149,7 +1269,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 } });
@@ -1253,9 +1373,42 @@ const JumpSelectionTests = struct {
try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col);
}
- test "jump selection clears copied rectangles in both history directions" {
+ test "a B1 sweep over several lines is a stream, as acme's, in a body and a tag" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("alpha\nbravo\ncharlie\n");
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect);
+ // From the press to the end of its line, the line between whole, the
+ // last from its start: no line numbers, no rectangle.
+ drag(p, x + 2, y, x + 1, y + 2);
+ try std.testing.expectEqualStrings("pha\nbravo\nch", pardes.test_api.heldSelection(p, 0).?);
+ // Upward, the same text.
+ drag(p, x + 1, y + 2, x + 2, y);
+ try std.testing.expectEqualStrings("pha\nbravo\nch", pardes.test_api.heldSelection(p, 0).?);
+ // A tag of several rows sweeps the same way.
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = try p.gpa.dupe(u8, "Save Del\nLook Edit");
+ p.sync();
+ _ = try p.render(frame.allocator());
+ p.sync();
+ try std.testing.expectEqual(@as(u16, 2), pane.tag_rows);
+ const sl: panes.Pane.Sel = .{ .state = .done, .r0 = 0, .c0 = 3, .r1 = 1, .c1 = 1 };
+ const text = try pardes.edit.paneText(p, pane);
+ const swept = try pardes.edit.selectionText(p, pane, sl);
+ const first_line = text[0 .. std.mem.indexOfScalar(u8, text, '\n') orelse text.len];
+ const second = std.mem.sliceTo(text[first_line.len + 1 ..], '\n');
+ const want = try std.fmt.allocPrint(frame.allocator(), "{s}\n{s}", .{ first_line[3..], second[0..@min(2, second.len)] });
+ try std.testing.expectEqualStrings(want, swept);
+ }
+
+ test "jump selection clears copied sweeps in both history directions" {
for ([_]bool{ false, true }) |reverse|
- try exercise("alpha\nbravo\n", false, reverse, 1, 3, "lph\nrav");
+ try exercise("alpha\nbravo\n", false, reverse, 1, 3, "lpha\nbrav");
}
test "jump selection clears wrapped Unicode and tab rectangles" {
@@ -1325,22 +1478,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;
@@ -1366,7 +1526,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");
@@ -1376,17 +1536,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);
}
@@ -1409,7 +1572,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);
@@ -1428,7 +1591,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);
@@ -1570,7 +1733,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");
@@ -1582,13 +1745,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 } });
@@ -1596,6 +1761,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 });
@@ -1618,7 +1809,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);
@@ -1666,7 +1857,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());
@@ -2319,7 +2510,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" {
@@ -2672,7 +2863,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);
@@ -2720,7 +2911,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);
@@ -2790,7 +2981,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);
@@ -3105,8 +3296,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 };
@@ -3346,7 +3537,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 };
@@ -3401,7 +3592,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());
@@ -3449,8 +3640,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());
@@ -3488,7 +3679,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 };
@@ -3525,7 +3716,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
@@ -3607,7 +3798,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());
@@ -3685,8 +3876,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.
@@ -3737,6 +3928,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" } });
@@ -3745,7 +3938,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());
@@ -3781,7 +3974,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());
@@ -3814,10 +4007,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| {
@@ -3857,7 +4050,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;
@@ -3895,7 +4088,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
@@ -3933,7 +4126,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
@@ -3958,6 +4151,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;
@@ -3967,7 +4162,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;
@@ -4010,12 +4205,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();
@@ -4063,11 +4258,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();
@@ -4117,8 +4312,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) {
@@ -4212,11 +4407,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();
@@ -4294,7 +4489,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
@@ -4424,9 +4619,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));
@@ -4467,6 +4662,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");
@@ -4483,8 +4680,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));
@@ -4533,7 +4730,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());