summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 13:09:12 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit84fca2d6e5f498f2d4384712d49d727a25441d68 (patch)
tree47a6d01ee65c5d631f59bee2f1efc5685ceb95f4 /src
parent047a2b50a7ac0162d7ef38f912d41c15a47479c7 (diff)
downloadpardes-84fca2d6e5f498f2d4384712d49d727a25441d68.tar.gz
pardes-84fca2d6e5f498f2d4384712d49d727a25441d68.zip
A click that takes no text tells the event reader nothing, and click records are tested for their offsets
Dogfooding saw a body right-click reported as ML0 0 2 3 qty. On file panes the offsets were right in every case tried here (a scratch made over 9P and clicked while another pane is active, after a hover, scrolled, soft-wrapped, tab-indented, inside parentheses); the test now holds them. The 0 0 form is a terminal's body, which has no offsets to give (its body is a history snapshot); since the event file now takes a record back whole, such a record can still be written back and done, and the docs say so. What the test did find: a click on whitespace sent an empty record, where acme's look3 and execute return without a word on an empty expansion (look.c:37, exec.c:141). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/mouse.zig7
-rw-r--r--src/ninep/events.zig56
2 files changed, 59 insertions, 4 deletions
diff --git a/src/mouse.zig b/src/mouse.zig
index beb2e624..0ec20df2 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -715,10 +715,9 @@ fn dispatchPointerBuiltin(
const arg = p.chord_arg;
p.chord_arg = null;
defer if (arg) |a| p.gpa.free(a);
- const operand = text orelse {
- if (gesture) |g| _ = reportGesture(p, id, cmd, "", g.on_tag, g.operand, arg != null);
- return;
- };
+ // A click that takes no text does nothing, and tells an event reader
+ // nothing, as acme's look3 and execute return on an empty expansion.
+ const operand = text orelse return;
if (gesture) |g| if (reportGesture(p, id, cmd, operand, g.on_tag, g.operand, arg != null)) return;
exec.runBuiltin(p, cmd, id, "", exec.withArg(p, operand, arg));
}
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 3f36c65f..9ebb4bc1 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -921,3 +921,59 @@ test "opens of the log share the open records, and a closed one frees its record
for (handles) |h| _ = call(p, .{ .tag = 5, .op = .release, .node = log, .handle = h });
for (p.fs.opens) |o| try testing.expect(o.node == 0);
}
+
+test "a click in the body reports the offsets of the word it takes, so the record can be written back" {
+ const config = @import("../config.zig");
+ const gpa = testing.allocator;
+ const text = "alpha qty beta\n qty\n\tqty\ndef f(qty): return qty.x\n" ++ "w" ** 100 ++ " qty\n";
+ const p = try withFile(gpa, text);
+ defer p.deinit();
+ // A scratch made over 9P, clicked while another pane has the keyboard.
+ const scratch = try th.newPane(p);
+ _ = wr(p, Node.of(scratch, .body), text);
+ for ([_]u32{ serialOf(p), scratch }) |serial| {
+ const id = p.paneBySerial(serial).?;
+ p.active = p.paneBySerial(if (serial == scratch) serialOf(p) else scratch).?;
+ const event = Node.of(serial, .event);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = event }).reply.handle;
+ var frame = std.heap.ArenaAllocator.init(gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[id];
+ const x0 = rect.x + config.GUTTER + config.PREFIX_W;
+ const y0 = if (p.settings.tag_bottom) rect.y else rect.y + p.panes[id].?.tag_rows;
+ const tab: u16 = config.tab_width;
+ const Click = struct { x: u16, y: u16, look: []const u8, exec: []const u8, scroll: i32 = 0 };
+ // Where the wrapped line's `qty` landed, as drawn.
+ const surface = try p.render(frame.allocator());
+ var wrap_col: u16 = 0;
+ while (wrap_col < rect.w and !std.mem.eql(u8, surface.at(x0 + wrap_col, y0 + 5).grapheme(), "q")) wrap_col += 1;
+ for ([_]Click{
+ .{ .x = x0 + 7, .y = y0, .look = "ML6 9 0 3 qty\n", .exec = "MX6 9 0 3 qty\n" },
+ .{ .x = x0 + 5, .y = y0 + 1, .look = "ML19 22 0 3 qty\n", .exec = "MX19 22 0 3 qty\n" },
+ .{ .x = x0 + tab + 1, .y = y0 + 2, .look = "ML24 27 0 3 qty\n", .exec = "MX24 27 0 3 qty\n" },
+ .{ .x = x0 + 7, .y = y0 + 3, .look = "ML34 37 0 3 qty\n", .exec = "MX34 37 0 3 qty\n" },
+ // Scrolled a line, the first row shows the second.
+ .{ .x = x0 + 5, .y = y0, .look = "ML19 22 0 3 qty\n", .exec = "MX19 22 0 3 qty\n", .scroll = 1 },
+ // Wrapped: the word on the continuation row.
+ .{ .x = x0 + wrap_col, .y = y0 + 5, .look = "ML154 157 0 3 qty\n", .exec = "MX154 157 0 3 qty\n" },
+ }) |c| for ([_]pardes.Mouse.Button{ config.look_button, config.exec_button }, [_][]const u8{ c.look, c.exec }) |button, want| {
+ // The pointer rests on the word first, as a hand's does, and
+ // the hover preview comes up under it.
+ p.panes[id].?.scrollBy(c.scroll);
+ _ = try p.render(frame.allocator());
+ p.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = c.x, .row = c.y } });
+ for (0..200) |_| p.update(.tick);
+ p.update(.{ .mouse = .{ .button = button, .kind = .press, .col = c.x, .row = c.y } });
+ p.update(.{ .mouse = .{ .button = button, .kind = .release, .col = c.x, .row = c.y } });
+ try testing.expectEqualStrings(want, call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).bytes);
+ p.panes[id].?.scrollBy(-c.scroll);
+ };
+ // A click on nothing expands to nothing, and acme then says
+ // nothing (look.c:37, exec.c:141).
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .press, .col = x0 + 60, .row = y0 + 1 } });
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = x0 + 60, .row = y0 + 1 } });
+ try testing.expectEqual(Status.again, call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).reply.status);
+ _ = call(p, .{ .tag = 3, .op = .release, .node = event, .handle = h });
+ }
+}