diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 00:01:57 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 23f430573efe4f6ef55b0180aa0d742ff4eca1d9 (patch) | |
| tree | da2157ba73d6abff4b1674541433294fb6c2daa9 /test | |
| parent | 9584b0682b45f17aaa42157540947904d867777a (diff) | |
| download | pardes-23f430573efe4f6ef55b0180aa0d742ff4eca1d9.tar.gz pardes-23f430573efe4f6ef55b0180aa0d742ff4eca1d9.zip | |
Make a pane's tag a Text edited with the body's own keys
The tag was a fixed buffer with its own one-line editor, a cursor measured
across the path, and a hijack of the body's mode while it was typed into.
It is now the pane's second Text: its own characters after the computed
prefix (path, dirty marker, PDF page), which is never stored in it, so a
rename or a dirty marker never moves its cursor. Normal and insert mode are
the body's, undo included; `:` moves the keyboard between body and tag, the
tag remembers its cursor and starts on Save, and executing a word from it
hands the keyboard back. The mouse still sees the prefix: a sweep selects
across it and a click on the path drafts a new name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'test')
| -rw-r--r-- | test/mode.zig | 31 | ||||
| -rw-r--r-- | test/panes.zig | 384 | ||||
| -rw-r--r-- | test/pdf.zig | 23 | ||||
| -rw-r--r-- | test/snapshots/cmdword.golden | 4 | ||||
| -rw-r--r-- | test/snapshots/cmdword.snap | 24 | ||||
| -rw-r--r-- | test/snapshots/collapse.golden | 4 | ||||
| -rw-r--r-- | test/snapshots/collapse.snap | 7 | ||||
| -rw-r--r-- | test/snapshots/tag.snap | 20 | ||||
| -rw-r--r-- | test/snapshots/tagalign.snap | 9 | ||||
| -rw-r--r-- | test/snapshots/tagcmd.snap | 20 | ||||
| -rw-r--r-- | test/snapshots/tagnav.golden | 43 | ||||
| -rw-r--r-- | test/snapshots/tagnav.snap | 136 | ||||
| -rw-r--r-- | test/snapshots/tagpath.golden | 23 | ||||
| -rw-r--r-- | test/snapshots/tagpath.snap | 28 | ||||
| -rw-r--r-- | test/snapshots/ttyclick.golden | 4 | ||||
| -rw-r--r-- | test/snapshots/ttyclick.snap | 14 |
16 files changed, 360 insertions, 414 deletions
diff --git a/test/mode.zig b/test/mode.zig index 19d59699..c429ea72 100644 --- a/test/mode.zig +++ b/test/mode.zig @@ -6,7 +6,7 @@ const Pardes = pardes.Pardes; fn mode(p: *Pardes, id: usize, expected: panes.Text.Mode) !void { try std.testing.expect(p.executeBuiltinLine(id, "Mode")); try std.testing.expectEqual(expected, p.panes[id].?.body.mode); - try std.testing.expect(!p.panes[id].?.tag_edit); + try std.testing.expect(p.panes[id].?.focus == .body); } fn clickModeTag(p: *Pardes) !void { @@ -17,13 +17,12 @@ fn clickModeTag(p: *Pardes) !void { const row = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; for ([_]pardes.Mouse.Kind{ .press, .release }) |kind| p.update(.{ .mouse = .{ .button = .left, .kind = kind, .col = rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col)), .row = row } }); - try std.testing.expect(pane.tag_edit); + try std.testing.expect(pane.focus == .tag); } -fn customTag(pane: *pardes.Pane, text: []const u8) void { - @memcpy(pane.tag_tail[0..text.len], text); - pane.tag_tail_len = text.len; - pane.tag_init = true; +fn customTag(pane: *pardes.Pane, text: []const u8) !void { + if (pane.tag.own) |own| pane.gpa.free(own); + pane.tag.own = try pane.gpa.dupe(u8, text); } test "Mode cycles terminal modes and keeps legacy toggle and custom tags" { @@ -37,14 +36,18 @@ test "Mode cycles terminal modes and keeps legacy toggle and custom tags" { try std.testing.expect(std.mem.indexOf(u8, try p.tagText(p.scratch.allocator(), pane), " Mode") != null); try mode(p, 0, .normal); try mode(p, 0, .insert); + // A clicked tag is typed into; its execute key is a normal-mode key, as + // a body's is, and the body keeps its own mode meanwhile. try clickModeTag(p); - try std.testing.expectEqual(panes.Text.Mode.insert, pane.tag_mode); + try std.testing.expectEqual(panes.Text.Mode.insert, pane.tag.mode); + try std.testing.expectEqual(panes.Text.Mode.insert, pane.body.mode); + p.update(.{ .key = .{ .cp = pardes.Key.escape } }); p.update(.{ .key = .{ .cp = pardes.Key.tab } }); try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); - try std.testing.expect(!pane.tag_edit); + try std.testing.expect(pane.focus == .body); while (p.nextEffect()) |effect| try std.testing.expect(effect != .write); - customTag(pane, " Togglettymode Keep"); + try customTag(pane, " Togglettymode Keep"); try mode(p, 0, .normal); try mode(p, 0, .insert); try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode")); @@ -53,7 +56,7 @@ test "Mode cycles terminal modes and keeps legacy toggle and custom tags" { try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode); p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); - try std.testing.expectEqualStrings(" Togglettymode Keep", pane.tag_tail[0..pane.tag_tail_len]); + try std.testing.expectEqualStrings(" Togglettymode Keep", pane.tag.own.?); } test "Mode from actual tag Exec preserves parked file and output insert transactions" { @@ -65,17 +68,17 @@ test "Mode from actual tag Exec preserves parked file and output insert transact const pane = try p.setTestFile("ab\n"); if (output) pane.file.?.output = .{ .from = .search }; try std.testing.expect(std.mem.indexOf(u8, try p.tagText(p.scratch.allocator(), pane), " Mode") == null); - customTag(pane, " Mode"); + try customTag(pane, " Mode"); try mode(p, 0, .insert); p.update(.{ .key = .{ .cp = '界', .text = "界" } }); p.update(.{ .key = .{ .cp = 'x', .text = "x" } }); try std.testing.expectEqualStrings("界xab\n", pane.file.?.content); try clickModeTag(p); - try std.testing.expectEqual(panes.Text.Mode.insert, pane.tag_mode); - if (normal_tag) pane.body.mode = .normal; + try std.testing.expectEqual(panes.Text.Mode.insert, pane.body.mode); + if (normal_tag) pane.tag.mode = .normal else p.update(.{ .key = .{ .cp = pardes.Key.escape } }); p.update(.{ .key = .{ .cp = pardes.Key.tab } }); try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode); - try std.testing.expect(!pane.tag_edit); + try std.testing.expect(pane.focus == .body); try std.testing.expect(pane.body.append_at == null); p.update(.{ .key = .{ .cp = 'u' } }); try std.testing.expectEqualStrings("ab\n", pane.file.?.content); 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 => { diff --git a/test/pdf.zig b/test/pdf.zig index 5bc271d4..745ecb1a 100644 --- a/test/pdf.zig +++ b/test/pdf.zig @@ -309,8 +309,8 @@ test "PDF dump fallback remains a byte-preserving file" { try std.testing.expect(!hasPdf(pane)); try std.testing.expectEqualStrings(path, pane.file.?.path); try std.testing.expectEqualSlices(u8, source, pane.file.?.content); - try std.testing.expect(pane.tag_init); - try std.testing.expectEqualStrings(" Keep Del", pane.tag_tail[0..pane.tag_tail_len]); + try std.testing.expect(pane.tag.own != null); + try std.testing.expectEqualStrings(" Keep Del", pane.tag.own.?); } test "PdfSections Look follows the exact owning PDF, not an equal path" { @@ -918,9 +918,8 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" { try std.testing.expectEqual(PdfTintMode.full, pane.pdf.?.tint); pane.pdf.?.fit = .height; const custom_tail = " Keep Del"; - @memcpy(pane.tag_tail[0..custom_tail.len], custom_tail); - pane.tag_tail_len = custom_tail.len; - pane.tag_init = true; + if (pane.tag.own) |own| p.gpa.free(own); + pane.tag.own = try p.gpa.dupe(u8, custom_tail); try pardes.dump.dumpState(p); const restored = try pardes.dump.initFromDump(gpa, .{}, p.dump_out.?); defer restored.deinit(); @@ -929,10 +928,10 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" { try std.testing.expectEqual(PdfFitMode.width, restored.panes[0].?.pdf.?.fit); try std.testing.expectEqual(PdfTintMode.filtered, restored.panes[0].?.pdf.?.tint); const restored_pane = restored.panes[0].?; - try std.testing.expect(restored_pane.tag_init); + try std.testing.expect(restored_pane.tag.own != null); try std.testing.expectEqualStrings( custom_tail, - restored_pane.tag_tail[0..restored_pane.tag_tail_len], + restored_pane.tag.own.?, ); } @@ -1100,20 +1099,20 @@ test "PDF normal adapter consumes unsupported actions and navigates page fallbac try std.testing.expectEqual(@as(u32, 0), pane.body.normal.count); try std.testing.expectEqual(.none, pane.body.normal.prefix); try std.testing.expect(!pane.body.vsel.active and !pane.body.msel.active and pane.body.nsel == 0); - try std.testing.expect(!pane.tag_edit); + try std.testing.expect(pane.focus == .body); // Cross-pane BODY-NORMAL actions keep their established shared paths. p.update(.{ .key = .{ .cp = ':' } }); - try std.testing.expect(pane.tag_edit); + try std.testing.expect(pane.focus == .tag); try std.testing.expectEqual(Mode.normal, pane.body.mode); - p.update(.{ .key = .{ .cp = Key.escape } }); - try std.testing.expect(!pane.tag_edit); + p.update(.{ .key = .{ .cp = ':' } }); + try std.testing.expect(pane.focus == .body); p.update(.{ .key = .{ .cp = ' ' } }); try std.testing.expect(p.leader_on); p.update(.{ .key = .{ .cp = '!' } }); try std.testing.expect(!p.leader_on); p.update(.{ .key = .{ .cp = '/' } }); - try std.testing.expect(pane.prompt == .search and !pane.tag_edit); + try std.testing.expect(pane.prompt == .search and pane.focus == .body); p.update(.{ .key = .{ .cp = Key.escape } }); try std.testing.expect(pane.prompt == .none); } diff --git a/test/snapshots/cmdword.golden b/test/snapshots/cmdword.golden index e6f470fa..f099464f 100644 --- a/test/snapshots/cmdword.golden +++ b/test/snapshots/cmdword.golden @@ -16,9 +16,9 @@ == snap nested grid=120x31 cursor=4,24 |22: ls |23: cmds.txt p.txt zzmark.txt -== snap execword grid=120x31 cursor=69,17 +== snap execword grid=120x31 cursor=4,24 |17: /tmp/pardes-snap/cmdword/cwd Tty Save Mode Filter Collapse Del Exec -== snap tagcmd grid=120x31 cursor=83,17 +== snap tagcmd grid=120x31 cursor=4,24 |17: /tmp/pardes-snap/cmdword/cwd Tty Save Mode Filter Collapse Del Exec @`echo ZZ''T` |24: echo ZZ''T |25: ZZT diff --git a/test/snapshots/cmdword.snap b/test/snapshots/cmdword.snap index eb0e0ea6..361e37ba 100644 --- a/test/snapshots/cmdword.snap +++ b/test/snapshots/cmdword.snap @@ -57,25 +57,31 @@ wait 10000 zzmark.txt stable 700 15000 snap nested # the same syntax typed into a TAG — the other place the expansion has to know -# about backticks (tagChordText). The look above left the shell focused, so `:` -# opens ITS tag; `i` then End walks to the end of the tail, and the chord runs -# the word under the cursor exactly as the body's does. +# about backticks. The look above left the shell focused, so `:` opens ITS +# tag; `A` appends at the end of it, Esc and `h` put the normal-mode cursor on +# the last character typed, and the execute key runs the word under it +# exactly as the body's does. key : -key i -key end +key A key space text Exec -key left +key esc +settle 100 +key h key tab stable 700 8000 # ...except that word is `Exec` ITSELF, with nothing to execute. This is the # case that must not hang a re-entrant dispatcher: it runs out of argument -# immediately and does nothing at all. +# immediately and does nothing at all. Executing gave the keyboard back to the +# body, and `:` finds the tag's cursor where it was left. snap execword -key end +key : +key A key space text @`echo ZZ''T` -key left +key esc +settle 100 +key h key tab wait 10000 ZZT stable 700 15000 diff --git a/test/snapshots/collapse.golden b/test/snapshots/collapse.golden index 18d24d6b..eaf9bbc3 100644 --- a/test/snapshots/collapse.golden +++ b/test/snapshots/collapse.golden @@ -68,8 +68,8 @@ |3: |4: |5: -== 9p tab-collapsed-cursor grid=160x25 cursor=51,2,bar -== snap tab-restored grid=160x25 cursor=51,2 +== 9p tab-collapsed-cursor grid=160x25 cursor=none +== snap tab-restored grid=160x25 cursor=7,3 |3: 1 KEEP-FIRST Collapse |4: 2 KEEP-SECOND |5: 3 KEEP-THIRD diff --git a/test/snapshots/collapse.snap b/test/snapshots/collapse.snap index 64f6bdb8..83872855 100644 --- a/test/snapshots/collapse.snap +++ b/test/snapshots/collapse.snap @@ -1,5 +1,6 @@ # A default pane-tag command collapses to one row and restores the body. -# Exercise real middle clicks twice, then tag editing's Tab execution twice. +# Exercise real middle clicks twice, then the tag's own execute key twice: a +# click types into the tag, Esc is its normal mode, and Tab executes there. file kept.txt KEEP-FIRST\nKEEP-SECOND\nKEEP-THIRD start 25 160 kept.txt wait 8000 KEEP-THIRD @@ -19,12 +20,16 @@ stable 500 8000 snap middle-restored press left @Collapse 3 release left @Collapse 3 +key esc +settle 100 key tab stable 500 8000 snap tab-collapsed snap9p tab-collapsed-cursor press left @Collapse 3 release left @Collapse 3 +key esc +settle 100 key tab wait 8000 KEEP-THIRD stable 500 8000 diff --git a/test/snapshots/tag.snap b/test/snapshots/tag.snap index a545ff9f..4faf6062 100644 --- a/test/snapshots/tag.snap +++ b/test/snapshots/tag.snap @@ -1,7 +1,8 @@ -# a CLICKED tag is insert mode: type straight in (no 'i'; `:` is the normal- -# mode door, see tagcmd); backspace edits; Esc leaves back to the body; Enter -# looks the word at the cursor (Tab executes it). Multi-word execution from -# the tag stays the mouse's job (middle-drag), like anywhere else. +# a CLICKED tag is typed straight into (no 'i'; `:` is the normal-mode door, +# see tagcmd); backspace edits; Esc drops to the tag's normal mode, which is +# the body's normal mode, and `:` there hands the keyboard back to the body; +# Enter looks the word at the cursor (Tab executes it). Multi-word execution +# from the tag stays the mouse's job (middle-drag), like anywhere else. file t.txt ZZLOOKED start 31 150 -n 3 wait 8000 t.txt @@ -29,16 +30,21 @@ snap tag-del send .txt stable 600 8000 stable 400 5000 -# Esc: out of the tag, body back in normal mode, typed tail stays as text +# Esc: the tag's normal mode; `:` then gives the keyboard back to the body, +# which is in normal mode, and the typed text stays key esc settle 100 +key : stable 400 5000 snap tag-exit -# re-enter the tag and look the typed word: Enter opens t.txt as a file pane. -# Click the inserted word itself; filename, separator and commands are compact. +# re-enter the tag and look the typed word: Enter in normal mode opens t.txt +# as a file pane. Click the inserted word itself; filename, separator and +# commands are compact. press left @t.txt 3 release left @t.txt 3 stable 400 5000 +key esc +settle 100 key enter wait 10000 ZZLOOKED stable 700 15000 diff --git a/test/snapshots/tagalign.snap b/test/snapshots/tagalign.snap index 9df22f57..a77415d9 100644 --- a/test/snapshots/tagalign.snap +++ b/test/snapshots/tagalign.snap @@ -22,9 +22,14 @@ press left 12 3 release left 12 3 stable 400 5000 snap clicked -# End reveals the complete command suffix; a scrolled Del still executes. +# The click was on the path, so it drafted a name; Esc drops the draft. `:` +# then `A` puts the caret after the tag's end, which reveals the complete +# command suffix; a scrolled Del still executes. resize 31 70 -key end +key esc +settle 100 +key : +key A stable 400 5000 snap revealed press middle @Del 3 diff --git a/test/snapshots/tagcmd.snap b/test/snapshots/tagcmd.snap index 305ed521..0e9047fb 100644 --- a/test/snapshots/tagcmd.snap +++ b/test/snapshots/tagcmd.snap @@ -1,11 +1,12 @@ # `:` — vim's command line with acme's vocabulary. It focuses the pane's OWN # tag in NORMAL mode (block cursor; the layout box shows the pane's BODY mode, -# which for this file pane is normal either way), parked at the tail's -# START — always the start, so `:w` is the same two keystrokes every time. -# Motions walk the tag's words (w selects the "Save" span, exactly like a body -# word motion), the execute chord runs the one under the cursor and hands the -# body back in normal mode — same keys as the body, so Tab by default (see -# config.look_key/exec_key). Esc backs out instead, leaving the tail as it was. +# which for this file pane is normal either way). The tag remembers where its +# cursor was; the first time there is nothing to remember and it starts on +# Save. The tag's normal mode is the body's: w selects the "Save" span exactly +# as a body word motion does, and the execute key runs the word under the +# cursor and hands the body back — same keys as the body, so Tab by default +# (see config.look_key/exec_key). `:` again backs out instead, leaving the tag +# as it was. lines c.txt 6 start 31 100 c.txt wait 8000 Newcol Joincol @@ -18,7 +19,7 @@ key esc settle 100 stable 400 5000 snap edited -# `:` -> the tag, in normal mode, cursor on the tail's first cell +# `:` -> the tag, in normal mode, cursor on Save key : stable 400 5000 snap colon @@ -32,10 +33,9 @@ stable 600 8000 stable 400 5000 snap saved snapfile written /tmp/pardes-snap/tagcmd/cwd/c.txt -# `:` then Esc: back to the body with the tail untouched, nothing run +# `:` then `:`: back to the body with the tag untouched, nothing run key : stable 400 5000 -key esc -settle 100 +key : stable 400 5000 snap escaped diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden index 52915940..3fdba9f2 100644 --- a/test/snapshots/tagnav.golden +++ b/test/snapshots/tagnav.golden @@ -22,39 +22,20 @@ == snap colon grid=100x31 cursor=28,2 |2: es-snap/tagnav/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte |18: ls -== snap right-tag grid=100x31 cursor=29,2 -== snap left-tag grid=100x31 cursor=28,2 -== snap down-tag grid=100x31 cursor=7,3 +== snap word grid=100x31 cursor=32,2 +== snap in-tag grid=100x31 cursor=34,2 +== snap right grid=100x31 cursor=54,5 |2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte -== snap up-tag grid=100x31 cursor=7,3 -== snap column-tag grid=100x31 cursor=7,3 -== snap up-edge grid=100x31 cursor=7,3 -== snap topbar-word grid=100x31 cursor=11,3 -== snap topbar-graphemes grid=100x31 cursor=7,4 -== snap column-down grid=100x31 cursor=7,5 -== snap back-down grid=100x31 cursor=7,5 -== snap arrows-still-motion grid=100x31 cursor=9,5 -== snap back-to-body grid=100x31 cursor=4,20 -|3: 1 alpha Last ls +|3: 1 alpha Right ls +== snap right-tag grid=100x31 cursor=70,2 +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll p/tagnav/cwd Tty Save Mode Filter Collapse Del +|3: 1 alpha ls +== snap left-tag grid=100x31 cursor=43,2 +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte +== snap back-to-body grid=100x31 cursor=7,3 == snap tty-on grid=100x31 cursor=54,5 |2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll$ /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte |3: 1 alpha $ ls |5: 3 gamma $ -== snap tty-tag grid=100x31 cursor=57,5 -|5: 3 gamma $ h:l -== snap tty-restored grid=100x31 cursor=4,20 -|3: 1 alpha $ ls Last -== snap topbar-dollar grid=100x31 cursor=48,17 -|3: 1 alpha $ ls -|17: p/tagnav/cwd Tty Save Mode Filter Collapse Del -== snap topbar-dump-word grid=100x31 cursor=25,17 -== snap topbar-exec grid=100x31 cursor=4,20 -|17: /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte -|29: /tmp/pardes-snap/tagnav/cwd/+Search Save Tty Co -|30: 1 -== snap topbar-restore-word grid=100x31 cursor=49,17 -|17: p/pardes-snap/tagnav/cwd Tty Save Mode Filter C -== snap topbar-restored grid=100x31 cursor=4,20 -|17: /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte -|27: /tmp/pardes-snap/tagnav/cwd/+Search Save Tty Co -|28: 1 +== snap tty-tag grid=100x31 cursor=90,2 +== snap tty-restored grid=100x31 cursor=54,5 diff --git a/test/snapshots/tagnav.snap b/test/snapshots/tagnav.snap index c375e644..eed808de 100644 --- a/test/snapshots/tagnav.snap +++ b/test/snapshots/tagnav.snap @@ -1,25 +1,22 @@ -# h/j/k/l ON A TAGLINE move focus BETWEEN taglines. A tag is a place in the -# LAYOUT, not just a line of text, so the four letters walk the layout and land -# on the neighbour's TAG (top row of the window), still in normal mode — you -# walk the taglines and never drop through a body. They run the SAME -# Left/Down/Up/Right builtins `SPC w h/j/k/l` and `Ctrl-w h/j/k/l` run: one -# focusDir, three routes to it. The ARROWS keep the in-tag grapheme motion, so -# no capability was traded away. +# A tag's normal mode is the body's: h/l/w move in the tag's own text, and +# moving between panes is the window keys' job from a tag as from a body -- +# there are no tag-only hops. `:` is the one key a tag and a body do not +# share: from the body it focuses the pane's own tag, from the tag it hands the +# keyboard back to the body. Each tag remembers its cursor; the first time +# there is nothing to remember and it starts on Save. # # Evidence is the `cursor=` header (the ACTIVE pane's cursor, which while a tag # is focused sits on THAT pane's tag row) plus each pane's layout box, which -# carries its BODY mode as one character: `•` normal, `^` insert, `$` tty. A -# tag edit parks the body mode rather than overwriting it, so the box keeps -# saying what the pane we left will be when we hop back off its tagline. +# carries its BODY mode as one character: ` ` normal, `^` insert, `$` tty. The +# tag has a mode of its own, so the box never changes while a tag is used. file w.txt alpha\nbeta\ngamma start 31 100 w.txt wait 8000 Newcol Joincol wait 8000 w.txt stable 700 20000 -# A FILE boots alone now, so the shell this script calls "the launch shell" is -# opened here: Alt-n opens it and focuses it, Alt-c puts it in a column of its -# own on the right, SPC w h comes back to the file. (`New`/`Newcol` open an -# empty +New scratch now, not a shell.) +# A FILE boots alone, so the launch shell is opened here: Alt-n opens it and +# focuses it, Alt-c puts it in a column of its own on the right, SPC w h comes +# back to the file. key a-n key a-c stable 700 15000 @@ -32,110 +29,51 @@ stable 700 15000 key space w k stable 400 5000 snap start -# `:` -> the file's OWN tagline, normal mode, parked at the tail's start +# `:` -> the file's OWN tagline, normal mode, on Save key : stable 400 5000 snap colon -# l -> right, onto the launch shell's TAGLINE (not its body) -key l -stable 400 5000 -snap right-tag -# h -> back left onto the file's tagline -key h -stable 400 5000 -snap left-tag -# j -> down onto the tagline of the shell under the file -key j -stable 400 5000 -snap down-tag -# k -> back up onto the file's -key k -stable 400 5000 -snap up-tag -# k visits the column tag first, then the workspace tag. Both stay in normal -# navigation mode; neither hop drops into a pane body. -key k -stable 400 5000 -snap column-tag -key k -stable 400 5000 -snap up-edge -# the bar speaks the tag's own one-line vocabulary: w to the next word start +# w selects in the tag the way it selects in a body key w stable 400 5000 -snap topbar-word -# h/l are plain graphemes up here — row 0 has no window to its left or right to -# walk to — and the arrows still mean what they always did -key l right +snap word +# l is a motion in the tag, not a hop to the next pane +key l l stable 400 5000 -snap topbar-graphemes -# j is the mirror of the k that got you here: back DOWN onto the topmost pane's -# tagline, still in normal mode, so the hop is a round trip -key j +snap in-tag +# the window keys move between panes from a tag as from a body: the tag lets +# go and the neighbour's BODY takes the keyboard +key c-w l +stable 400 5000 +snap right +# `:` there -> that pane's own tag, on its Save the first time +key : stable 400 5000 -snap column-down -key j +snap right-tag +# back left, and `:` finds the file's tag cursor where l left it +key c-w h stable 400 5000 -snap back-down -# the arrows are still in-tag motion: two rights walk two columns of the tail -key right right +key : stable 400 5000 -snap arrows-still-motion -key esc -settle 100 +snap left-tag +# `:` in the tag's normal mode: back to the body +key : stable 400 5000 snap back-to-body -# A TERMINAL MUST NOT LOSE ITS TTY MODE. Put the launch shell in raw tty mode -# (Ctrl-b), come back, then hop onto its tagline and off again: exitTagEdit -# restores the body mode the tag hijacked, and its box reads `$` throughout — -# during the hop too, because the box shows the PARKED mode, not the tag's. +# A TERMINAL KEEPS ITS TTY MODE. Put the launch shell in raw tty mode (Ctrl-b), +# click its tagline and come back: its box reads `$` throughout, because the +# tag's mode is the tag's own. key c-w l stable 400 5000 key c-b stable 700 15000 snap tty-on -key c-w h -stable 400 5000 -key : -key l +press left @Mode 3 +release left @Mode 3 stable 400 5000 snap tty-tag key esc settle 100 -stable 400 5000 -snap tty-restored -# Enter/Tab on the BAR runs the word under the cursor through the very dispatch -# a middle click on it uses, and leaves the bar FIRST — `Kill` lives up here, so -# a builtin that frees the session must never return into a focused topbar (the -# same hazard the pane-tag chord has with `Del`). `$` proves the destructive -# word is exactly last; four long-word steps back land on `Dump`, the visible -# non-destructive action used here. It grows a `Restore <path>` word, which is -# also the case where the line gets longer than the column you were parked on. -key c-w h -stable 400 5000 -key : -key k k -stable 400 5000 -key $ -stable 400 5000 -snap topbar-dollar -key B B B B -stable 400 5000 -snap topbar-dump-word -key enter -stable 700 15000 -snap topbar-exec -# THE DELETION HAZARD, from the bar itself: `Restore` frees every pane and -# swaps the core out from under the caller. Walk back onto the word the Dump -# just grew — B twice off the tail, because the LONG word motions are the ones -# that step over a path in one go — and run it. The bar drops focus BEFORE the -# builtin, so nothing reaches for state the swap already freed. key : -key k k stable 400 5000 -key $ B B -stable 400 5000 -snap topbar-restore-word -key enter -stable 900 15000 -snap topbar-restored +snap tty-restored diff --git a/test/snapshots/tagpath.golden b/test/snapshots/tagpath.golden index 1b798445..2ba1a2e6 100644 --- a/test/snapshots/tagpath.golden +++ b/test/snapshots/tagpath.golden @@ -8,12 +8,12 @@ | 4 line 4 | 5 == snap colon grid=100x31 cursor=38,2 -== snap col0 grid=100x31 cursor=3,2 -== snap sel-path grid=100x31 cursor=36,2 -== style sel-path grid=100x31 +== snap col0 grid=100x31 cursor=37,2 +== snap drag-cross grid=100x31 cursor=41,2 +== style drag-cross grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-36 #f5f7ee,#3c614a, 37-58 #becfc4,#26362d, 59-99 d,#26362d, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-10 #becfc4,#26362d, 11-41 #f5f7ee,#3c614a, 42-58 #becfc4,#26362d, 59-99 d,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-99 d,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-99 d,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-99 d,#0d1410, @@ -42,16 +42,9 @@ |28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410, |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410, -== snap yanked-path grid=100x31 cursor=41,3 -|2: /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del -|3: 1 l/tmp/pardes-snap/tagpath/cwd/c.txtine 1 -== snap drag-cross grid=100x31 cursor=41,2 -== style drag-cross grid=100x31 -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-10 #becfc4,#26362d, 11-41 #f5f7ee,#3c614a, 42-60 #becfc4,#26362d, 61-99 d,#26362d, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-46 #e5eee5,#0d1410, 47-99 d,#0d1410, == snap name-draft grid=100x31 cursor=11,2 -|2: /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del -== snap name-cancelled grid=100x31 cursor=41,3 -|2: /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del -== snap name-committed grid=100x31 cursor=41,3 +|2: /renamed/tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del +== snap name-cancelled grid=100x31 cursor=7,3 +|2: /tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del +== snap name-committed grid=100x31 cursor=7,3 |2: /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del diff --git a/test/snapshots/tagpath.snap b/test/snapshots/tagpath.snap index cf431fa1..b9ef80e6 100644 --- a/test/snapshots/tagpath.snap +++ b/test/snapshots/tagpath.snap @@ -1,5 +1,8 @@ -# Filename drafts share the selectable tag coordinate space with commands. -# Escape cancels; Enter changes buffer identity, never renames a disk file. +# The path is part of the tag's text for the mouse -- a sweep selects across +# the path and the commands after it -- but never for the keyboard: the tag's +# cursor lives in its own text, so `0` goes to the start of that, not of the +# path. A click on the path drafts a new name there instead. Escape cancels; +# Enter changes buffer identity, never renames a disk file. lines c.txt 4 start 31 100 c.txt wait 8000 Newcol Joincol @@ -12,16 +15,6 @@ snap colon key 0 stable 400 5000 snap col0 -key E -stable 400 5000 -snap sel-path -snapstyle sel-path -key y -key esc -settle 100 -key p -stable 400 5000 -snap yanked-path # Sweep across the path / command boundary without injected padding. press left 12 3 drag left 30 3 @@ -33,17 +26,18 @@ snapstyle drag-cross key esc settle 100 key : -key 0 -key i +stable 400 5000 +# a click on the path is a name draft, the caret where it was clicked +press left 4 3 +release left 4 3 text /renamed stable 400 5000 snap name-draft key esc stable 400 5000 snap name-cancelled -key : -key 0 -key i +press left 4 3 +release left 4 3 text /renamed key enter stable 400 5000 diff --git a/test/snapshots/ttyclick.golden b/test/snapshots/ttyclick.golden index 719e3955..14344371 100644 --- a/test/snapshots/ttyclick.golden +++ b/test/snapshots/ttyclick.golden @@ -20,11 +20,7 @@ | ls | probe.txt == snap tty-tagedit grid=100x31 cursor=33,2 -|3: ls ls -|5: == snap tty-tagexit grid=100x31 cursor=4,5 -|3: $ ls ls -|5: $ == snap ins-bodyclick grid=100x31 cursor=11,4 |2: ^ /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil |3: ls ls diff --git a/test/snapshots/ttyclick.snap b/test/snapshots/ttyclick.snap index 89ed5300..c125a231 100644 --- a/test/snapshots/ttyclick.snap +++ b/test/snapshots/ttyclick.snap @@ -1,7 +1,7 @@ # clicks in a shell pane never change its mode: a body click in raw tty -# keeps tty (before: the press dropped it to normal), a tag-edit round trip -# restores the mode it hijacked, and an insert-mode body click re-pins the -# cursor but stays insert. File panes keep the old behavior (click -> normal). +# keeps tty (before: the press dropped it to normal), a round trip through the +# tag leaves it alone (the tag has a mode of its own), and an insert-mode body +# click re-pins the cursor but stays insert. File panes keep the old behavior (click -> normal). # The mode is the character in each pane's layout box: `•` normal, `^` insert, # `$` tty. file probe.txt marker @@ -15,14 +15,16 @@ press left 10 5 release left 10 5 stable 400 5000 snap tty-bodyclick -# tag edit from tty: the box still reads `$` while the tag is being edited (the -# body mode is PARKED, which is the invariant), and Esc hands it back — a bug -# that lost it would show `•` in one snap or the other +# tag edit from tty: the box still reads `$` while the tag is typed into (the +# tag's mode is its own), and Esc then `:` hands the keyboard back — a bug +# that lost the body's mode would show `•` in one snap or the other press left @Tty 3 release left @Tty 3 stable 400 5000 snap tty-tagedit key esc +settle 100 +key : stable 400 5000 snap tty-tagexit # out of tty, into insert: a body click moves the cursor but keeps INS |
