diff options
Diffstat (limited to 'test/panes.zig')
| -rw-r--r-- | test/panes.zig | 384 |
1 files changed, 201 insertions, 183 deletions
diff --git a/test/panes.zig b/test/panes.zig index f1adafee..a391dba2 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -12,6 +12,17 @@ const image = pardes.image; const syntax = pardes.syntax; const filesystem = pardes.filesystem; +/// The tag's caret as a byte offset into the tag as it is shown, prefix +/// included. +fn tagCaret(p: *Pardes, pane: *Pane) !usize { + return (try p.tagPrefix(pane)).len + @as(usize, @intCast(pane.tag.cur_col)); +} + +fn setTag(pane: *Pane, text: []const u8) !void { + if (pane.tag.own) |own| pane.gpa.free(own); + pane.tag.own = try pane.gpa.dupe(u8, text); +} + const TagLayerTests = struct { const Kind = @FieldType(pardes.TagLayer, "kind"); fn findLayer(p: *Pardes, kind: Kind, id: usize) !*pardes.TagLayer { @@ -36,10 +47,7 @@ const TagLayerTests = struct { p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 }; p.gpa.free(pane.file.?.path); pane.file.?.path = try p.gpa.dupe(u8, "/x"); - const tail = " " ++ "a" ** 38 ++ "\t界 END"; - @memcpy(pane.tag_tail[0..tail.len], tail); - pane.tag_tail_len = tail.len; - pane.tag_init = true; + try setTag(pane, " " ++ "a" ** 38 ++ "\t界 END"); var frame = std.heap.ArenaAllocator.init(p.gpa); defer frame.deinit(); _ = try p.render(frame.allocator()); @@ -55,11 +63,11 @@ const TagLayerTests = struct { var stale = mouse(layer, x, .press); stale.tag_hit.?.serial +%= 1; p.update(.{ .mouse = stale }); - try std.testing.expect(!pane.tag_edit); + try std.testing.expect(pane.focus == .body); p.update(.{ .mouse = mouse(layer, x, .press) }); p.update(.{ .mouse = mouse(layer, x, .release) }); - try std.testing.expect(pane.tag_edit); - try std.testing.expectEqual(@as(u16, @intCast(at)), pane.tag_col); + try std.testing.expect(pane.focus == .tag); + try std.testing.expectEqual(at, try tagCaret(p, pane)); p.update(.{ .key = .{ .cp = 'X', .text = "X" } }); _ = try p.render(frame.allocator()); const edited = try p.tagText(frame.allocator(), pane); @@ -91,7 +99,7 @@ const TagLayerTests = struct { event.row = @intFromFloat(painted_y); p.update(.{ .mouse = event }); } - try std.testing.expectEqual(@as(u16, @intCast(at)), pane.tag_col); + try std.testing.expectEqual(at, try tagCaret(p, pane)); } } @@ -119,19 +127,17 @@ const TagLayerTests = struct { try std.testing.expectEqualStrings(" ", p.surface.at(layer.viewport.x - 1, layer.viewport.y).grapheme()); p.update(.{ .mouse = mouse(layer, edge - 0.1, .press) }); try std.testing.expect(p.drag != .move); - try std.testing.expect(!p.panes[second].?.tag_edit); + try std.testing.expect(p.panes[second].?.focus == .body); p.update(.{ .mouse = mouse(layer, edge - 0.1, .release) }); p.update(.{ .mouse = mouse(layer, edge + 0.1, .press) }); p.update(.{ .mouse = mouse(layer, edge + 0.1, .release) }); try std.testing.expectEqual(second, p.active); - try std.testing.expect(p.panes[second].?.tag_edit); - try std.testing.expectEqual(@as(u16, 0), p.panes[second].?.tag_col); + // The first cell of the tag is its path: a click there drafts a name. + try std.testing.expect(p.panes[second].?.prompt == .name); + try std.testing.expectEqual(@as(i32, 0), p.panes[second].?.input.cur_col); p.update(.{ .key = .{ .cp = Key.escape } }); const left = p.panes[0].?; - const tail = " " ++ "a" ** 110; - @memcpy(left.tag_tail[0..tail.len], tail); - left.tag_tail_len = tail.len; - left.tag_init = true; + try setTag(left, " " ++ "a" ** 110); _ = try p.render(frame.allocator()); const left_layer = try findLayer(p, .pane, 0); const left_text = try p.tagText(frame.allocator(), left); @@ -142,8 +148,8 @@ const TagLayerTests = struct { p.update(.{ .mouse = mouse(layer, beyond_x, .release) }); try std.testing.expectEqual(@as(usize, 0), p.active); const end_col = left_layer.capacity(p.row_metrics) - 1; - try std.testing.expectEqual(@as(u16, @intCast(panes.File.rawAtDisplay(left_text, end_col))), left.tag_col); - try std.testing.expect(left.tag_sel); + try std.testing.expectEqual(panes.File.rawAtDisplay(left_text, end_col), try tagCaret(p, left)); + try std.testing.expect(left.sel[0].state == .done and left.sel[0].r0 == 0); p.update(.{ .key = .{ .cp = Key.escape } }); const long = "a" ** 60 ++ "界END"; try std.testing.expect(p.global_tag.set(long)); @@ -167,12 +173,12 @@ const TagNavigationTests = struct { p.update(.{ .key = .{ .cp = cp } }); } - fn enterNormal(p: *Pardes) *Pane { - key(p, ':'); - return p.panes[p.active].?; + fn window(p: *Pardes, letter: u21) void { + p.update(.{ .key = .{ .cp = 'w', .ctrl = true } }); + key(p, letter); } - test "tag navigation retains each pane cursor and mouse placement overrides memory" { + test "a tag remembers its cursor, starts on Save, and a click moves it" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 }); defer p.deinit(); const first = try p.setTestFile("body\n"); @@ -183,65 +189,97 @@ const TagNavigationTests = struct { try std.testing.expect(layout.splitColumn(p, second_id, second_id, false)); p.update(.tick); p.active = 0; + // Nothing to remember yet: `:` lands on Save, in normal mode. key(p, ':'); + try std.testing.expect(first.focus == .tag and first.tag.mode == .normal); + const tail = Pardes.curTail(first); + try std.testing.expectEqualStrings("Save", tail[@intCast(first.tag.cur_col)..][0..4]); key(p, Key.home); for (0..3) |_| key(p, Key.right); - const first_col = first.tag_col; - key(p, Key.escape); + try std.testing.expectEqual(@as(i32, 3), first.tag.cur_col); + // `:` in the tag's normal mode is the way back to the body. + key(p, ':'); + try std.testing.expect(first.focus == .body); p.active = second_id; key(p, ':'); key(p, Key.home); for (0..5) |_| key(p, Key.right); - const second_col = second.tag_col; - key(p, Key.escape); + key(p, ':'); p.active = 0; key(p, ':'); - try std.testing.expectEqual(first_col, first.tag_col); - key(p, Key.escape); + try std.testing.expectEqual(@as(i32, 3), first.tag.cur_col); + key(p, ':'); p.active = second_id; key(p, ':'); - try std.testing.expectEqual(second_col, second.tag_col); - key(p, Key.escape); + try std.testing.expectEqual(@as(i32, 5), second.tag.cur_col); + key(p, ':'); var frame = std.heap.ArenaAllocator.init(p.gpa); 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; - ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + 6, y); + const prefix: u16 = @intCast(panes.File.displayWidth(try p.tagPrefix(first))); + ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + prefix + 6, y); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(@as(u16, 6), first.tag_col); + try std.testing.expect(first.focus == .tag and first.tag.mode == .insert); + try std.testing.expectEqual(@as(i32, 6), first.tag.cur_col); key(p, Key.escape); + key(p, ':'); p.startPrompt(first, .{ .search = config.search_marker }); key(p, Key.escape); key(p, ':'); - try std.testing.expectEqual(@as(u16, 6), first.tag_col); - key(p, Key.escape); + try std.testing.expectEqual(@as(i32, 6), first.tag.cur_col); + key(p, ':'); try std.testing.expectEqualStrings("body\n", first.file.?.content); } - test "tag navigation clamps remembered columns to renamed graphemes and shorter tags" { + test "executing a word from the tag runs it and gives the keyboard back to the body" { + for ([_]u21{ Key.tab, Key.enter }) |chord| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + p.update(.{ .key = .{ .cp = 'i' } }); + p.update(.{ .key = .{ .cp = 'x', .text = "x" } }); + p.update(.{ .key = .{ .cp = Key.escape } }); + while (p.nextEffect()) |_| {} + // `:` lands on Save; the execute key runs it, the look key looks + // it up. Either way the tag lets go of the keyboard first. + key(p, ':'); + try std.testing.expect(pane.focus == .tag); + key(p, chord); + try std.testing.expect(pane.focus == .body); + try std.testing.expectEqual(.normal, pane.body.mode); + var saved = false; + while (p.nextEffect()) |effect| { + if (effect == .save_file) saved = true; + } + try std.testing.expectEqual(chord == Key.tab, saved); + // ...and the tag still knows where it was. + key(p, ':'); + try std.testing.expectEqualStrings("Save", Pardes.curTail(pane)[@intCast(pane.tag.cur_col)..][0..4]); + } + } + + test "a renamed path never moves the tag's cursor and a shorter tag pulls it back" { 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"); key(p, ':'); key(p, Key.home); for (0..3) |_| key(p, Key.right); - key(p, Key.escape); + key(p, ':'); p.gpa.free(pane.file.?.path); pane.file.?.path = try p.gpa.dupe(u8, "/界.zig"); key(p, ':'); - try std.testing.expectEqual(@as(u16, 1), pane.tag_col); - key(p, Key.end); - key(p, Key.escape); - p.gpa.free(pane.file.?.path); - pane.file.?.path = try p.gpa.dupe(u8, "/é"); - pane.tag_tail_len = 0; + try std.testing.expectEqual(@as(i32, 3), pane.tag.cur_col); + key(p, ':'); + try setTag(pane, "é"); key(p, ':'); - try std.testing.expectEqual(@as(u16, 3), pane.tag_col); + try std.testing.expectEqual(@as(i32, 2), pane.tag.cur_col); try std.testing.expectEqualStrings("body\n", pane.file.?.content); } - test "tag navigation J and K restore cursors in stacked panes" { + test "window keys move between panes from a tag and each tag keeps its cursor" { for ([_]bool{ false, true }) |bottom| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); @@ -253,88 +291,67 @@ const TagNavigationTests = struct { const second = p.panes[second_id].?; p.update(.tick); p.active = 0; - _ = enterNormal(p); + key(p, ':'); key(p, Key.home); key(p, 'l'); key(p, 'l'); - key(p, 'J'); + window(p, 'j'); try std.testing.expectEqual(second_id, p.active); - try std.testing.expect(!first.tag_edit and second.tag_edit); + try std.testing.expect(first.focus == .body and second.focus == .body); + key(p, ':'); key(p, Key.home); for (0..4) |_| key(p, 'l'); - key(p, 'K'); + window(p, 'k'); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(@as(u16, 2), first.tag_col); - key(p, 'J'); - try std.testing.expectEqual(second_id, p.active); - try std.testing.expectEqual(@as(u16, 4), second.tag_col); + try std.testing.expect(second.focus == .body); + key(p, ':'); + try std.testing.expectEqual(@as(i32, 2), first.tag.cur_col); + window(p, 'j'); + key(p, ':'); + try std.testing.expectEqual(@as(i32, 4), second.tag.cur_col); } } - test "tag navigation uses uppercase pane and header keys while lowercase stays local" { + test "a tag's normal mode is the body's: motions stay in the tag and insert types into it" { for ([_]bool{ false, true }) |bottom| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 }); defer p.deinit(); _ = try p.setTestFile("body\n"); p.presentation.enabled = false; p.settings.tag_bottom = bottom; - p.settings.column_tags = true; try std.testing.expect(p.executeBuiltinLine(0, "New")); const right = p.active; try std.testing.expect(layout.splitColumn(p, right, right, false)); p.update(.tick); p.active = 0; - const pane = enterNormal(p); + key(p, ':'); + const pane = p.panes[0].?; key(p, Key.home); key(p, 'l'); - try std.testing.expectEqual(@as(u16, 1), pane.tag_col); + try std.testing.expectEqual(@as(i32, 1), pane.tag.cur_col); key(p, 'h'); key(p, 'k'); - try std.testing.expectEqual(@as(u16, 0), pane.tag_col); + key(p, 'j'); + try std.testing.expectEqual(@as(i32, 0), pane.tag.cur_col); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expect(pane.tag_edit and p.topbar_col == null); - key(p, 'L'); + try std.testing.expect(pane.focus == .tag and p.topbar_col == null); + window(p, 'l'); try std.testing.expectEqual(right, p.active); - try std.testing.expect(!pane.tag_edit and p.panes[right].?.tag_edit); - key(p, 'H'); + try std.testing.expect(pane.focus == .body and p.panes[right].?.focus == .body); + window(p, 'h'); try std.testing.expectEqual(@as(usize, 0), p.active); - key(p, 'K'); - try std.testing.expect(p.topbar_col != null and p.header_column != null); - try std.testing.expect(!pane.tag_edit); - key(p, 'l'); - const column_cursor = p.topbar_col.?; - key(p, 'K'); - try std.testing.expect(p.topbar_col != null and p.header_column == null); - key(p, 'l'); - key(p, 'l'); - const workspace_cursor = p.topbar_col.?; - key(p, 'J'); - try std.testing.expect(p.topbar_col != null and p.header_column != null); - try std.testing.expectEqual(column_cursor, p.topbar_col.?); - key(p, 'K'); - try std.testing.expectEqual(workspace_cursor, p.topbar_col.?); - key(p, 'J'); - key(p, 'J'); - try std.testing.expect(p.topbar_col == null and pane.tag_edit); - key(p, 'j'); - try std.testing.expect(!pane.tag_edit and p.topbar_col == null); - try std.testing.expectEqual(@as(usize, 0), p.active); - _ = enterNormal(p); - key(p, 'K'); - key(p, 'j'); - try std.testing.expect(p.topbar_col == null and !pane.tag_edit); key(p, ':'); key(p, 'i'); key(p, Key.end); - const before = pane.tag_tail_len; - try std.testing.expect(pane.tag_edit and pane.body.mode == .insert); - for ("hjklHJKL") |letter| { + const before = Pardes.curTail(pane).len; + try std.testing.expect(pane.focus == .tag and pane.tag.mode == .insert and pane.body.mode == .normal); + for ("hjklHJKL:") |letter| { const text = [_]u8{letter}; p.update(.{ .key = .{ .cp = letter, .text = &text } }); - try std.testing.expect(pane.tag_edit and pane.body.mode == .insert); + try std.testing.expect(pane.focus == .tag and pane.tag.mode == .insert); } - try std.testing.expectEqualStrings("hjklHJKL", pane.tag_tail[before..pane.tag_tail_len]); - try std.testing.expect(pane.tag_edit and pane.body.mode == .insert and p.topbar_col == null); + try std.testing.expectEqualStrings("hjklHJKL:", Pardes.curTail(pane)[before..]); + try std.testing.expectEqualStrings("body\n", pane.file.?.content); try std.testing.expectEqual(@as(usize, 0), p.active); } } @@ -1151,32 +1168,26 @@ const TagNameTintTests = struct { } } - test "tag filename tint follows scrolled Unicode staged names and yields to selection" { + test "tag filename tint follows scrolled 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"); - const old = pane.file.?.path.len; configure(p); defer p.custom_theme = null; - p.update(.{ .key = .{ .cp = ':' } }); - p.update(.{ .key = .{ .cp = 'i' } }); - pane.tag_anchor = 0; - pane.tag_col = @intCast(old - 1); - pane.tag_sel = true; const name = "/a/very/long/directory/that/extends/past/the/tag/界e\u{301}name-with-a-long-tail.zig"; - p.update(.{ .paste = name }); - try std.testing.expectEqualStrings(name, pane.tag_name[0..pane.tag_name_len.?]); - pane.tag_col = @intCast(std.mem.indexOf(u8, name, "界").?); - pane.tag_sel = false; + p.draftName(pane, 0); + redraft(pane, name); + pane.input.cur_col = @intCast(std.mem.indexOf(u8, name, "界").?); try std.testing.expect((try check(p, 0, name)) > 0); try std.testing.expect(pane.tag_scroll > 0); - pane.tag_anchor = pane.tag_col; - pane.tag_sel = true; + // 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 var arena = std.heap.ArenaAllocator.init(p.gpa); defer arena.deinit(); const surface = try p.render(arena.allocator()); - const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.rawDisplayCol(name, pane.tag_col) - pane.tag_scroll)); + 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; 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); @@ -1208,32 +1219,46 @@ const TagNameTintTests = struct { } }; -test "pane filename edits stage commit and cancel without changing body or command tail" { +/// Replace a name draft's text, the caret after it, as clearing it and +/// typing would. +fn redraft(pane: *Pane, name: []const u8) void { + if (pane.input.own) |own| pane.gpa.free(own); + pane.input.own = pane.gpa.dupe(u8, name) catch unreachable; + pane.input.cur_col = @intCast(name.len); +} + +/// 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 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); + pane.input.cur_col = @intCast(pane.input.own.?.len); +} + +test "pane filename drafts 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()); + try pane.appendTag(" SyntaxBold"); + const tail = try p.gpa.dupe(u8, Pardes.curTail(pane)); 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; + clickPathEnd(p, pane); + p.update(.{ .key = .{ .cp = 'u', .ctrl = 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("/new name.zig", pane.input.own.?); + try std.testing.expectEqualStrings(tail, Pardes.curTail(pane)); 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.expect(pane.focus == .body); + try std.testing.expect(pane.prompt == .none); try std.testing.expectEqualStrings(if (commit) "/new name.zig" else original, pane.file.?.path); - try std.testing.expectEqualStrings(tail, pane.tagSlice()); + try std.testing.expectEqualStrings(tail, Pardes.curTail(pane)); 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); @@ -1243,27 +1268,21 @@ 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; + clickPathEnd(p, pane); + p.update(.{ .key = .{ .cp = 'u', .ctrl = true } }); p.update(.{ .paste = "/café界" }); p.update(.{ .key = .{ .cp = Key.backspace } }); - try std.testing.expectEqualStrings("/café", pane.tag_name[0..pane.tag_name_len.?]); + try std.testing.expectEqualStrings("/café", pane.input.own.?); 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.?); + try std.testing.expectEqualStrings("/caf", pane.input.own.?); + p.update(.{ .key = .{ .cp = 'u', .ctrl = true } }); + try std.testing.expectEqualStrings("", pane.input.own.?); p.update(.{ .key = .{ .cp = Key.enter } }); - try std.testing.expect(pane.tag_edit); + try std.testing.expect(pane.prompt == .name); 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); + try std.testing.expect(pane.prompt == .none); } test "leaving a pane or entering a header cancels an uncommitted filename" { @@ -1273,11 +1292,10 @@ test "leaving a pane or entering a header cancels an uncommitted filename" { 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(.tick); + clickPathEnd(p, pane); p.update(.{ .paste = "/draft" }); - try std.testing.expect(pane.tag_name_len != null); + try std.testing.expect(pane.prompt == .name); if (header) { p.topbar_col = 0; p.update(.tick); @@ -1285,21 +1303,18 @@ test "leaving a pane or entering a header cancels an uncommitted filename" { 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.expect(pane.prompt == .none); + try std.testing.expect(pane.focus == .body); try std.testing.expectEqualStrings("/test.txt", pane.file.?.path); } } -test "middle clicking Save commits the staged filename before issuing a save" { +test "middle clicking Save commits the drafted 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; + clickPathEnd(p, pane); + p.update(.{ .key = .{ .cp = 'u', .ctrl = true } }); p.update(.{ .paste = "/renamed.txt" }); var arena = std.heap.ArenaAllocator.init(p.gpa); defer arena.deinit(); @@ -1312,7 +1327,7 @@ test "middle clicking Save commits the staged filename before issuing a save" { 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.expect(pane.prompt == .none); try std.testing.expectEqualStrings("untouched\n", pane.file.?.content); var saves: usize = 0; while (p.nextEffect()) |effect| if (effect == .save_file) { @@ -1322,24 +1337,19 @@ test "middle clicking Save commits the staged filename before issuing a save" { try std.testing.expectEqual(@as(usize, 1), saves); } -test "oversized filename paste refuses atomically across a selected tag boundary" { +test "an oversized drafted name is refused at commit and the draft stays to correct" { 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); + clickPathEnd(p, pane); + const excess = try p.gpa.alloc(u8, 5000); defer p.gpa.free(excess); @memset(excess, 'x'); + excess[0] = '/'; 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)); + p.update(.{ .key = .{ .cp = Key.enter } }); + try std.testing.expect(pane.prompt == .name); + try std.testing.expectEqualStrings("/test.txt", pane.file.?.path); try std.testing.expectEqualStrings("untouched\n", pane.file.?.content); } @@ -1349,16 +1359,17 @@ test "paste into pane commands and prompts never inserts into the body" { 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")); + // A tag takes a paste as a body does, newline and all. + p.update(.{ .paste = " Custom\nCommand" }); + try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null); p.update(.{ .key = .{ .cp = Key.escape } }); p.startPrompt(pane, .{ .search = config.search_marker }); p.update(.{ .paste = "t\r\nwo" }); try std.testing.expectEqualStrings(config.search_marker ++ "two", pane.promptText().?); - try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), " CustomCommand")); + try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null); 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")); + try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null); } test "long pane tags reveal commands and map visible clicks to whole tag addresses" { @@ -1367,8 +1378,7 @@ test "long pane tags reveal commands and map visible clicks to whole tag address 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")); + try pane.appendTag(" SyntaxBold"); p.update(.{ .key = .{ .cp = ':' } }); p.update(.{ .key = .{ .cp = 'A' } }); var arena = std.heap.ArenaAllocator.init(p.gpa); @@ -1549,7 +1559,13 @@ test "terminal edits and undo survive partial history clearing and whole-screen pane.body.cur_pinned = true; pane.body.vsel = .{ .active = true, .row = row, .col = 1 }; panes.Terminal.pushUndo(p, pane); - pane.body.ed_redo[0] = panes.Terminal.snap(p, pane) orelse return error.SnapshotFailed; + const overlay = pane.ovl.?; + pane.body.ed_redo[0] = .{ + .ovl = .{ .row = overlay.row, .rows = overlay.rows, .text = try gpa.dupe(u8, overlay.text) }, + .cur_row = pane.body.cur_row, + .cur_col = pane.body.cur_col, + .vsel = pane.body.vsel, + }; pane.body.ed_redo_len = 1; if (action == .hidden_undo) { gpa.free(pane.ovl.?.text); @@ -1663,9 +1679,7 @@ test "a prompt's entry and cancellation leave the tag, the body and its cursor a try pane.setOwnedCwd("/dír space"); const tail = " keep"; for (cases) |case| { - @memcpy(pane.tag_tail[0..tail.len], tail); - pane.tag_tail_len = tail.len; - pane.tag_init = true; + try setTag(pane, tail); pane.body.cur_row = 1; pane.body.cur_col = 2; pane.body.cur_pinned = true; @@ -1677,17 +1691,17 @@ test "a prompt's entry and cancellation leave the tag, the body and its cursor a p.startPrompt(pane, .{ .search = config.search_marker }); } // The answer is typed into the prompt's own line, never the tag. - try std.testing.expect(!pane.tag_edit); + try std.testing.expect(pane.focus == .body); try std.testing.expectEqual(.insert, pane.input.mode); try std.testing.expectEqualStrings(case.marker, pane.promptText().?); try std.testing.expectEqual(@as(i32, @intCast(case.marker.len)), pane.input.cur_col); - try std.testing.expectEqualStrings(tail, pane.tag_tail[0..pane.tag_tail_len]); + try std.testing.expectEqualStrings(tail, pane.tag.own.?); try std.testing.expectEqualDeep(modal.Normal.State{}, pane.body.normal); p.update(.{ .key = .{ .cp = Key.escape } }); try std.testing.expect(pane.prompt == .none); try std.testing.expect(pane.input.own == null); try std.testing.expectEqual(.normal, pane.body.mode); - try std.testing.expectEqualStrings(tail, pane.tag_tail[0..pane.tag_tail_len]); + try std.testing.expectEqualStrings(tail, pane.tag.own.?); try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); try std.testing.expectEqual(@as(i32, 2), pane.body.cur_col); try std.testing.expectEqualStrings("one\ntwo\n", pane.file.?.content); @@ -1698,9 +1712,10 @@ test "a prompt opens whatever the tag already holds" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const pane = try p.setTestFile("one\n"); - @memset(&pane.tag_tail, 'x'); - pane.tag_tail_len = pane.tag_tail.len; - pane.tag_init = true; + const full = try p.gpa.alloc(u8, pardes.memory.limits.max_tag_tail); + defer p.gpa.free(full); + @memset(full, 'x'); + try setTag(pane, full); for (0..3) |case| { switch (case) { 0 => p.startPrompt(pane, .{ .search = config.search_marker }), @@ -1708,8 +1723,7 @@ test "a prompt opens whatever the tag already holds" { else => p.update(.{ .key = .{ .cp = '|' } }), } try std.testing.expect(pane.prompt != .none); - try std.testing.expectEqual(pane.tag_tail.len, pane.tag_tail_len); - for (pane.tag_tail) |byte| try std.testing.expectEqual(@as(u8, 'x'), byte); + try std.testing.expectEqualStrings(full, pane.tag.own.?); p.update(.{ .key = .{ .cp = Key.escape } }); } } @@ -1723,8 +1737,8 @@ test "generated output refuses every pipe prompt before seeding the tag" { for ([_]Key{ .{ .cp = '|' }, .{ .cp = '|', .alt = true }, .{ .cp = '!' }, .{ .cp = '!', .alt = true } }) |key| { p.update(.{ .key = key }); try std.testing.expect(pane.prompt == .none); - try std.testing.expect(!pane.tag_init); - try std.testing.expect(!pane.tag_edit); + try std.testing.expect(pane.tag.own == null); + try std.testing.expect(pane.focus == .body); try std.testing.expectEqualStrings("generated\n", pane.file.?.content); } } @@ -1798,7 +1812,7 @@ test "a prompt that cannot be opened leaves the open one as it was" { try std.testing.expect(pane.prompt == .search); try std.testing.expectEqualStrings(config.search_marker ++ "prior", pane.promptText().?); try std.testing.expectEqualDeep(previous_normal, pane.body.normal); - try std.testing.expect(!pane.tag_init and !pane.tag_edit); + try std.testing.expect(pane.tag.own == null and pane.focus == .body); } } @@ -1830,7 +1844,7 @@ test "a prompt opens with one allocation" { const before = failing.alloc_index; p.startPrompt(pane, request); try std.testing.expectEqual(before + 1, failing.alloc_index); - try std.testing.expect(!pane.tag_edit and pane.input.mode == .insert); + try std.testing.expect(pane.focus == .body and pane.input.mode == .insert); } } @@ -4080,14 +4094,18 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu 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 x = rect.x + config.GUTTER + @as(u16, @intCast(panes.File.gutterWidth(pane))) + 3; + const x = if (gesture == .tag) + rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(try p.tagPrefix(pane)))) + 3 + else + rect.x + config.GUTTER + @as(u16, @intCast(panes.File.gutterWidth(pane))) + 3; const y = if (gesture == .tag) tag_y else body_y; p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = x, .row = y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = x, .row = y } }); if (gesture == .tag) { - try std.testing.expect(pane.tag_edit); + try std.testing.expect(pane.focus == .tag); p.update(.{ .key = .{ .cp = Key.escape } }); - try std.testing.expect(!pane.tag_edit); + p.update(.{ .key = .{ .cp = ':' } }); + try std.testing.expect(pane.focus == .body); } }, .focus, .last => { |
