From a4a8b1874d477498fb427f8e2f63382c99f618a0 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 02:20:12 -0300 Subject: A msg said again word for word is that line counted in the log 1000 Undos with nothing left made 2000 log lines: the Verbose announcement and the answer each time. A msg repeating the newest record is now that record counted, (xN), as err's already were (shared as pushCounting), unless a follower has read it; and a builtin's own words that start with its name take the place of its announcement just before them. Co-Authored-By: Claude Opus 5.5 --- src/ninep/ctl.zig | 7 ++++-- src/ninep/events.zig | 71 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index d6922390..8a35c381 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -730,6 +730,9 @@ test "Undo and Redo on a pane's ctl step its body through its edits" { _ = wr(p, Node.of(serial, .ctl), "Undo"); try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status); try testing.expect(th.logHas(p, "Undo: nothing to undo")); + // Said again word for word, it is that line counted, as err's are. + for (0..3) |_| _ = wr(p, Node.of(serial, .ctl), "Undo"); + try testing.expect(th.logHas(p, "Undo: nothing to undo (x4)\n")); } test "focus reads the pane with the keyboard, and a serial written to it moves the keyboard there" { @@ -971,8 +974,8 @@ test "a message from a root ctl write is the session's in the log" { const frozen = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 8192 }).bytes.len; _ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" }); try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Kill\n").reply.status); - // The builtin's name, then what it said: both the session's. - try testing.expectEqualStrings("msg - Kill\n", call(p, .{ .tag = 4, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).bytes); + // What it said, the session's; its own words name it, so its Verbose + // announcement gives way to them. try testing.expectEqualStrings("msg - Kill: nothing running\n", call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).bytes); } diff --git a/src/ninep/events.zig b/src/ninep/events.zig index 7402a763..2112110a 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -145,7 +145,20 @@ pub fn noteLog(p: *Pardes, kind: LogKind, pane: *Pane) void { /// Records `msg ` for what the editor said, `-` for no pane. pub fn noteMessage(p: *Pardes, serial: u32, text: []const u8) void { var buf: [pardes.Messages.LoggedMessage.cap + 32]u8 = undefined; - pushLog(p, (if (serial == 0) + // A builtin's own word for what it did stands in for its Verbose + // announcement just before (`Undo` then `Undo: nothing to undo`), so + // that 1000 Undos with nothing left are one line, counted, as err's are. + if (newest(p)) |last| if (!followerRead(p, last.seq) and std.mem.startsWith(u8, last.text, "msg ")) { + const rest = last.text["msg ".len..]; + const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse rest.len; + const word = rest[@min(sp + 1, rest.len)..]; + var who: [16]u8 = undefined; + const mine = if (serial == 0) "-" else std.fmt.bufPrint(&who, "{d}", .{serial}) catch ""; + if (std.mem.eql(u8, rest[0..sp], mine) and word.len > 0 and std.mem.indexOfScalar(u8, word, ' ') == null and + text.len > word.len and std.mem.startsWith(u8, text, word) and text[word.len] == ':') + p.fs.log.buf.shrinkRetainingCapacity(last.at); + }; + pushCounting(p, (if (serial == 0) std.fmt.bufPrint(&buf, "msg - {s}\n", .{text}) else std.fmt.bufPrint(&buf, "msg {d} {s}\n", .{ serial, text })) catch return); @@ -196,10 +209,35 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void { for (record[0 .. record.len - 1]) |*c| if (c.* < ' ') { c.* = ' '; }; + pushCounting(p, record); +} + +/// Pushes `record`, or, when it repeats the newest record word for word, +/// counts that one instead: ` (x2)`. +fn pushCounting(p: *Pardes, record: []u8) void { // A client retrying a write that fails the same way would fill the ring // with one line, so a repeat of the newest record is that record counted, // `(x3)`, as +Messages counts its repeats; unless a follower has read it // already and so waits on the repeat as a line of its own. + const last = newest(p) orelse return pushLog(p, record); + var said = last.text; + var times: u32 = 1; + if (std.mem.endsWith(u8, said, ")")) if (std.mem.lastIndexOf(u8, said, " (x")) |x| { + if (std.fmt.parseInt(u32, said[x + 3 .. said.len - 1], 10)) |n| { + times = n; + said = said[0..x]; + } else |_| {} + }; + if (!std.mem.eql(u8, said, record[0 .. record.len - 1]) or followerRead(p, last.seq)) return pushLog(p, record); + var counted: [pardes.Messages.LoggedMessage.cap + 64]u8 = undefined; + const again = std.fmt.bufPrint(&counted, "{s} (x{d})\n", .{ said, times +| 1 }) catch return; + p.fs.log.buf.shrinkRetainingCapacity(last.at); + pushLog(p, again); +} + +/// The newest record in the ring: where it starts, its sequence number and +/// its text without the newline. +fn newest(p: *Pardes) ?struct { at: usize, seq: u64, text: []const u8 } { const q = &p.fs.log; var at = q.head; var last: ?usize = null; @@ -208,26 +246,18 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void { last = at; at += 4 + std.mem.readInt(u32, q.buf.items[at..][0..4], .little); } - const last_at = last orelse return pushLog(p, record); - const last_seq = seq - 1; - var said = q.buf.items[last_at + 4 ..]; - said = said[0 .. said.len - 1]; - var times: u32 = 1; - if (std.mem.endsWith(u8, said, ")")) if (std.mem.lastIndexOf(u8, said, " (x")) |x| { - if (std.fmt.parseInt(u32, said[x + 3 .. said.len - 1], 10)) |n| { - times = n; - said = said[0..x]; - } else |_| {} - }; - if (!std.mem.eql(u8, said, record[0 .. record.len - 1])) return pushLog(p, record); + const last_at = last orelse return null; + const text = q.buf.items[last_at + 4 ..]; + return .{ .at = last_at, .seq = seq - 1, .text = text[0 .. text.len - 1] }; +} + +/// Whether a follower has read the record `seq`, or is part way through it. +fn followerRead(p: *Pardes, seq: u64) bool { for (p.fs.opens) |o| if (o.node != 0 and o.what == .log) { const l = o.what.log; - if (l.follow and (l.next > last_seq or (l.next == last_seq and l.part > 0))) return pushLog(p, record); + if (l.follow and (l.next > seq or (l.next == seq and l.part > 0))) return true; }; - var counted: [280]u8 = undefined; - const again = std.fmt.bufPrint(&counted, "{s} (x{d})\n", .{ said, times +| 1 }) catch return; - q.buf.shrinkRetainingCapacity(last_at); - pushLog(p, again); + return false; } /// The log is one ring that records whether or not anyone reads it. A record @@ -851,10 +881,10 @@ test "the log records whether or not anyone reads, and an open that follows wait try testing.expectEqual(Status.again, rdf.next(p, log, fh, frozen).reply.status); // A shell's `read` asks for a few bytes at a time: the record comes in - // pieces rather than failing. Every repeat is its own line. + // pieces rather than failing. A repeat the follower waits on, having + // read the first, is its own line. const said_by = p.panes[0].?.serial; p.setMessage(0, "same failure"); - p.setMessage(0, "same failure"); var pieced: [64]u8 = undefined; var got: usize = 0; while (got == 0 or pieced[got - 1] != '\n') { @@ -863,6 +893,7 @@ test "the log records whether or not anyone reads, and an open that follows wait got += piece.len; } try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure\n", .{said_by}), pieced[0..got]); + p.setMessage(0, "same failure"); try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure\n", .{said_by}), rdf.next(p, log, fh, frozen).bytes); // A follower the ring outran hears how much it missed, then carries on. -- cgit v1.3