From 4de0be4942b5aad143949826523ce68a0b1b03cc Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 17:08:18 -0300 Subject: An empty range written back to event acts on the word a click there expands to, as acme's expand does `ML960 960` or `MX1 1` written back with no text acted on the empty range between the offsets: a look of nothing, an exec of nothing. A click at a point expands to the word or file name under it, and acme's written-back event does the same. It now expands with look.expandedWord, as a no-drag click does, on its line of the body or tag. fs.md says so. Co-Authored-By: Claude Opus 5.5 --- src/ninep/events.zig | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/ninep/events.zig b/src/ninep/events.zig index 63405781..fa90fe71 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -688,6 +688,15 @@ fn scanNumber(data: []const u8, i: *usize) ?u32 { } /// Writing a Look or Exec record back performs the action it names. +/// The word a click at `at` expands to: the file name or word run under it +/// on its line (look.expandedWord), else nothing. +fn expandAt(whole: []const u8, at: usize) []const u8 { + const start = if (std.mem.lastIndexOfScalar(u8, whole[0..at], '\n')) |nl| nl + 1 else 0; + const end = std.mem.indexOfScalarPos(u8, whole, at, '\n') orelse whole.len; + const b = look.expandedWord(whole[start..end], at - start) orelse return ""; + return whole[start + b.lo .. start + b.hi]; +} + pub fn writeEvent(p: *Pardes, asked: Req, id: usize) Reply { const pane0 = p.panes[id] orelse return Reply.fail(asked.tag, E.NOENT); // Its last record needs no newline, as every other line file's last @@ -723,7 +732,10 @@ pub fn writeEvent(p: *Pardes, asked: Req, id: usize) Reply { const whole = if (r.action.onTag()) pane_files.tagOf(p, pane) else pane_files.bodyOf(pane); const lo = @min(@as(usize, r.q0), whole.len); const hi = @max(lo, @min(@as(usize, r.q1), whole.len)); - const said = if (r.q0 == r.q1) r.text orelse whole[lo..hi] else whole[lo..hi]; + // An empty range with no text is a click at that point: expanded as + // the click would be, to the word or file name under it (acme's + // expand, look.c). + const said = if (r.q0 == r.q1) r.text orelse expandAt(whole, lo) else whole[lo..hi]; const text = p.scratch.allocator().dupe(u8, said) catch continue; switch (r.action) { // A body's click written back is done as the click would have @@ -1237,6 +1249,22 @@ test "a pane named before it is announced has its new under that name, and no re try testing.expect(std.mem.indexOf(u8, after.bytes, try std.fmt.bufPrint(&want, "rename {d} ", .{shell.serial})) == null); } +test "an empty range written back is expanded as a click at that point would be" { + const p = try withFile(testing.allocator, "abc def abc\nx Newcol y\n"); + defer p.deinit(); + const serial = serialOf(p); + const pane = p.panes[p.paneBySerial(serial).?].?; + const event = Node.of(serial, .event); + // A look at 1 is a look at `abc`: from dot, past the first, the next. + pane_files.setDot(pane, .{ .q0 = 4, .q1 = 4 }); + try testing.expectEqual(Status.ok, wr(p, event, "ML1 1\n").reply.status); + try testing.expectEqual(pane_files.State.Range{ .q0 = 8, .q1 = 11 }, pane_files.dotOf(pane)); + // An exec at 15 runs the word there, `Newcol`. + const cols = p.ncol; + try testing.expectEqual(Status.ok, wr(p, event, "MX15 15\n").reply.status); + try testing.expectEqual(cols + 1, p.ncol); +} + test "opens of the log share the open records, and a closed one frees its record" { const gpa = testing.allocator; const p = try withFile(gpa, "x\n"); -- cgit v1.3