From fc2738eea560769c8d150a7cc955fe8d54121298 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 12:54:29 -0300 Subject: Align file clicks and insertion cursors --- test/panes.zig | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) (limited to 'test/panes.zig') diff --git a/test/panes.zig b/test/panes.zig index ab7726e1..ded5b075 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -12,6 +12,139 @@ const image = pardes.image; const syntax = pardes.syntax; const filesystem = pardes.filesystem; +const ClickCursorTests = struct { + fn click(p: *Pardes, button: pardes.Mouse.Button, x: u16, y: u16) void { + p.update(.{ .mouse = .{ .button = button, .kind = .press, .col = x, .row = y } }); + p.update(.{ .mouse = .{ .button = button, .kind = .release, .col = x, .row = y } }); + } + + test "click cursor and insertion agree beyond line and file ends" { + for ([_]bool{ false, true }) |already_insert| { + for ([_]bool{ false, true }) |bottom_tag| { + for ([_]struct { text: []const u8, click_row: u16, row: i32, col: i32, result: []const u8 }{ + .{ .text = "abc\nxy", .click_row = 0, .row = 0, .col = 3, .result = "abcZ\nxy" }, + .{ .text = "abc\nxy", .click_row = 5, .row = 1, .col = 2, .result = "abc\nxyZ" }, + .{ .text = "abc\n", .click_row = 5, .row = 1, .col = 0, .result = "abc\nZ" }, + .{ .text = "", .click_row = 5, .row = 0, .col = 0, .result = "Z" }, + }) |case| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 }); + defer p.deinit(); + p.settings.tag_bottom = bottom_tag; + const pane = try p.setTestFile(case.text); + p.presentation.enabled = false; + if (already_insert) p.update(.{ .key = .{ .cp = 'i' } }); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); + const y = if (bottom_tag) rect.y else rect.y + pardes.BOX_H; + click(p, .left, x + 15, y + case.click_row); + try std.testing.expectEqual(case.row, pane.cur_row); + try std.testing.expectEqual(case.col, pane.cur_col); + const clicked = (try p.render(frame.allocator())).cursor orelse return error.MissingClickCursor; + try std.testing.expectEqual(x + @as(u16, @intCast(case.col)), clicked.x); + try std.testing.expectEqual(y + @as(u16, @intCast(case.row)), clicked.y); + if (!already_insert) p.update(.{ .key = .{ .cp = 'i' } }); + try std.testing.expectEqual(Pane.Mode.insert, pane.mode); + const insert = (try p.render(frame.allocator())).cursor orelse return error.MissingInsertCursor; + try std.testing.expectEqual(clicked.x, insert.x); + try std.testing.expectEqual(clicked.y, insert.y); + p.update(.{ .key = .{ .cp = 'Z', .text = "Z" } }); + try std.testing.expectEqualStrings(case.result, pane.file.?.content); + } + } + } + } + + test "click cursor snaps tab and wrapped wide glyph cells before insertion" { + for ([_]bool{ false, true }) |already_insert| { + for ([_]bool{ false, true }) |wrap| { + const source = if (wrap) "012345678901234567890123456789界tail\n" else "\t界tail\n"; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 24, .rows = 12 }); + defer p.deinit(); + const pane = try p.setTestFile(source); + p.settings.wrap = wrap; + p.presentation.enabled = false; + if (already_insert) p.update(.{ .key = .{ .cp = 'i' } }); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + const surface = try p.render(frame.allocator()); + var target: ?struct { x: u16, y: u16 } = null; + for (0..surface.rows) |row| for (0..surface.cols) |col| { + if (std.mem.eql(u8, surface.at(@intCast(col), @intCast(row)).grapheme(), "界")) + target = .{ .x = @intCast(col), .y = @intCast(row) }; + }; + const glyph = target orelse return error.MissingWideGlyph; + if (wrap) try std.testing.expect(glyph.y > p.rects[0].y + pardes.BOX_H); + click(p, .left, glyph.x + 1, glyph.y); + const byte = std.mem.indexOf(u8, source, "界").?; + try std.testing.expectEqual(@as(i32, @intCast(byte)), pane.cur_col); + const clicked = (try p.render(frame.allocator())).cursor orelse return error.MissingClickCursor; + try std.testing.expectEqual(glyph.x, clicked.x); + try std.testing.expectEqual(glyph.y, clicked.y); + if (!already_insert) p.update(.{ .key = .{ .cp = 'i' } }); + try std.testing.expectEqual(Pane.Mode.insert, pane.mode); + const insert = (try p.render(frame.allocator())).cursor orelse return error.MissingInsertCursor; + try std.testing.expectEqual(clicked.x, insert.x); + try std.testing.expectEqual(clicked.y, insert.y); + p.update(.{ .key = .{ .cp = 'Z', .text = "Z" } }); + const expected = try std.fmt.allocPrint(p.gpa, "{s}Z{s}", .{ source[0..byte], source[byte..] }); + defer p.gpa.free(expected); + try std.testing.expectEqualStrings(expected, pane.file.?.content); + } + } + } + + test "click operands and hover follow the scrolled source selection" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 14 }); + defer p.deinit(); + const pane = try p.setTestFile("zero\none\ntwo\nthree\nalpha beta\nfive\ngamma delta\nseven\n" ++ "tail\n" ** 20); + p.presentation.enabled = false; + p.settings.wrap = false; + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); + const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + JumpSelectionTests.drag(p, x + 1, y + 4, x + 3, y + 4); + try std.testing.expectEqualStrings("lph", pardes.test_api.heldSelection(p, 0).?); + for (0..2) |_| p.update(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = x + 2, .row = y + 4 } }); + try std.testing.expectEqual(@as(usize, 2), pane.file.?.scroll); + _ = try p.render(frame.allocator()); + const event_node = filesystem.Node.of(pane.serial, .event); + const opened = filesystem.handle(p, .{ .tag = 0, .op = .open, .node = event_node }); + try std.testing.expectEqual(.ok, opened.status); + defer _ = filesystem.handle(p, .{ .tag = 0, .op = .release, .node = event_node, .handle = opened.handle }); + + for ([_]pardes.Mouse.Button{ .middle, .right }) |button| { + const events = &p.fs.panes[0].events; + while (!events.empty()) events.pop(); + click(p, button, x + 2, y + 2); + const selected = events.peek() orelse return error.MissingSelectionGesture; + try std.testing.expect(std.mem.endsWith(u8, selected, " lph\n")); + try std.testing.expect(std.mem.startsWith(u8, selected[2..], "20 23 ")); + while (!events.empty()) events.pop(); + click(p, button, x + 2, y + 4); + const unselected = events.peek() orelse return error.MissingWordGesture; + try std.testing.expect(std.mem.endsWith(u8, unselected, " gamma\n")); + } + + const delay = config.look_preview_delay_frames orelse return; + p.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = x + 2, .row = y + 2 } }); + for (0..delay) |_| p.update(.tick); + const selected_preview = p.look_hover_preview orelse return error.MissingSelectionHover; + try std.testing.expect(selected_preview.sel != null); + try std.testing.expect(pane.pointerSelection(selected_preview.sel.?) != null); + p.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = x + 2, .row = y + 4 } }); + for (0..delay) |_| p.update(.tick); + const word_preview = p.look_hover_preview orelse return error.MissingWordHover; + try std.testing.expect(word_preview.file_word != null); + try std.testing.expectEqual(@as(i32, 6), word_preview.file_word.?.row); + } +}; + const JumpSelectionTests = struct { fn drag(p: *Pardes, col0: u16, row0: u16, col1: u16, row1: u16) void { p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = col0, .row = row0 } }); @@ -2691,6 +2824,7 @@ const TerminalTests = struct { }; test { + _ = ClickCursorTests; _ = JumpSelectionTests; _ = @import("output.zig"); _ = @import("pdf.zig"); -- cgit v1.3