diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index b87a5d6a..357a6b13 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -40,6 +40,7 @@ const runtime_cfg = @import("runtime_config.zig"); /// Every board-shaped capacity, in one table keyed on a profile rather than on /// the platform. See src/limits.zig. const limits = @import("limits.zig"); +const message = @import("message.zig"); const selection_pipe = @import("selection_pipe.zig"); /// acme's control filesystem, as a pure transaction over this core: the FILES /// a script opens (`body`, `ctl`, `event`, ...) and what they mean. The @@ -700,6 +701,50 @@ test "gj/gk step the wrapped rows a body draws while j/k keep the file's lines" try std.testing.expectEqual(@as(i32, 1), pane.cur_row); } +test "the message log keeps what the row forgets, and collapses repeats" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 }); + defer p.deinit(); + + p.setMessage(0, "14:32:07 saved /x.zig"); + p.setMessage(0, "14:32:09 saved /x.zig"); // same event, later clock + p.setMessage(0, "save: AccessDenied"); + + // Two entries, not three: the clock does not make a message new. This is + // the case the de-duplication exists for and the one it used to miss, + // because `message.stamp` makes every host message unique by construction. + try std.testing.expectEqual(@as(usize, 2), p.messages_len); + const first = p.messageLog(0).?; + try std.testing.expectEqual(@as(u16, 2), first.repeats); + // ...and the NEWEST wording is what survives, so the row carries the last + // time it happened rather than the first. + try std.testing.expectEqualStrings("14:32:09 saved /x.zig", first.slice()); + try std.testing.expectEqualStrings("save: AccessDenied", p.messageLog(1).?.slice()); + try std.testing.expect(p.messageLog(2) == null); + + // The same text from a DIFFERENT pane is a different event: one pane's + // failure must not be recorded as another's. + p.setMessage(1, "save: AccessDenied"); + try std.testing.expectEqual(@as(usize, 3), p.messages_len); + try std.testing.expectEqual(@as(u8, 1), p.messageLog(2).?.pane); + + // Progress is NOT logged: it arrives several times a second for a whole + // index and would push everything else out (`setStatus`). + p.setStatus(0, "14:32:10 lsp rust-analyzer: Indexing 47%"); + try std.testing.expectEqual(@as(usize, 3), p.messages_len); + + // The ring wraps rather than grows, and still reads oldest-first. + for (0..limits.message_log + 5) |i| { + var buf: [32]u8 = undefined; + p.setMessage(0, std.fmt.bufPrint(&buf, "line {d}", .{i}) catch unreachable); + } + try std.testing.expectEqual(@as(usize, limits.message_log), p.messages_len); + try std.testing.expectEqualStrings("line 5", p.messageLog(0).?.slice()); + var last_buf: [32]u8 = undefined; + const want_last = std.fmt.bufPrint(&last_buf, "line {d}", .{limits.message_log + 4}) catch unreachable; + try std.testing.expectEqualStrings(want_last, p.messageLog(limits.message_log - 1).?.slice()); +} + test "the acme chords act once per selection, not once on the primary" { const gpa = std.testing.allocator; const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 }); @@ -6132,6 +6177,24 @@ fn pipeMarker(how: normal_input.PipeBehavior) []const u8 { }; } +/// One line that was said on a message row. Fixed storage so the log cannot +/// fail: `setMessage` is reached from `reportError`, which is reached from +/// paths that are reporting an allocation failure. +pub const LoggedMessage = struct { + pub const cap = 256; + text: [cap]u8 = undefined, + len: u16 = 0, + /// Which pane said it, or 0xff for one said to no pane in particular. + pane: u8 = 0xff, + /// How many times in a row. Saturating: a key held down against the same + /// refusal is one event, not four hundred. + repeats: u16 = 0, + + pub fn slice(m: *const LoggedMessage) []const u8 { + return m.text[0..m.len]; + } +}; + const PendingPipe = struct { id: u32, pane: usize, @@ -6476,6 +6539,12 @@ pub const Pardes = struct { /// The in-program answers behind every unimplemented host method. Per /// instance, so several cores behind one fan-out host stay independent. fallback: Fallback, + /// The message-row log: a fixed ring, never allocated, never grown. See + /// `logMessage` and the `Messages` builtin. + messages: [limits.message_log]LoggedMessage = @splat(.{}), + /// Next slot to write. `messages_len` saturates at the ring's size. + messages_head: usize = 0, + messages_len: usize = 0, /// Input the loop has not consumed yet. Single-threaded: a host's worker /// threads keep their own thread-safe inbox and post from the loop thread, /// which is what keeps this ring lock-free. @@ -7214,12 +7283,79 @@ pub const Pardes = struct { /// mouse event — and nothing here knows whether the row is free: an armed /// prompt simply outranks a message at render time, so a message posted /// under one is stored and invisible rather than refused. + /// A message row that is NOT worth remembering: unsolicited progress from a + /// language server, which arrives several times a second for the whole of a + /// large index. + /// + /// `rust-analyzer: Indexing 47% core` is a different string every tick by + /// construction, so no de-duplication can collapse it, and at the client's + /// throttle of one per 150ms per server it takes about NINETEEN SECONDS to + /// push every save, error and reload out of a 128-entry ring. A log that + /// one indexing run empties is not a log. Progress belongs on the row, + /// where it is read as it happens and then replaced; the log is for things + /// that were said once. + pub fn setStatus(p: *Pardes, id: usize, text: []const u8) void { + p.showMessage(id, text); + } + pub fn setMessage(p: *Pardes, id: usize, text: []const u8) void { + // LOGGED FIRST, and logged even when the pane is gone. A message row + // is cleared by the next keystroke (see `clearMessages`), so anything + // said while the user was looking elsewhere — a save that failed, a + // watcher's reload, a builtin's complaint — used to be unrecoverable + // the instant it appeared. `Messages` reads this back. + p.logMessage(id, text); + p.showMessage(id, text); + } + + fn showMessage(p: *Pardes, id: usize, text: []const u8) void { if (id >= MAX_PANES) return; const pane = p.panes[id] orelse return; pane.msg_len = @intCast(@min(text.len, pane.msg.len)); @memcpy(pane.msg[0..pane.msg_len], text[0..pane.msg_len]); } + + /// Append to the ring, oldest overwritten. No allocation and no failure: + /// this sits under every `reportError` in the program, including the ones + /// raised because an allocation just failed. + fn logMessage(p: *Pardes, id: usize, text: []const u8) void { + if (text.len == 0) return; + const pane: u8 = if (id < MAX_PANES) @intCast(id) else 0xff; + // What is STORED is truncated to the slot, so what is COMPARED must be + // too: comparing a 300-byte message against its own 256-byte record + // never matched, and two identical long messages each got their own + // row. And the comparison is on `message.body` — the row without its + // clock — because a stamp makes every host message unique by + // construction, which defeated this entirely for `saved`/`reloaded`. + const kept = text[0..@min(text.len, LoggedMessage.cap)]; + if (p.messages_len > 0) { + const last = &p.messages[(p.messages_head + limits.message_log - 1) % limits.message_log]; + if (last.pane == pane and + std.mem.eql(u8, message.body(last.slice()), message.body(kept))) + { + // The NEWEST wording wins, so the row carries the latest clock + // rather than the moment the run started. + last.len = @intCast(kept.len); + @memcpy(last.text[0..last.len], kept); + last.repeats +|= 1; + return; + } + } + const slot = &p.messages[p.messages_head]; + slot.len = @intCast(kept.len); + @memcpy(slot.text[0..slot.len], kept); + slot.pane = pane; + slot.repeats = 1; + p.messages_head = (p.messages_head + 1) % limits.message_log; + p.messages_len = @min(p.messages_len + 1, limits.message_log); + } + + /// The log oldest-first, which is reading order. + pub fn messageLog(p: *const Pardes, i: usize) ?*const LoggedMessage { + if (i >= p.messages_len) return null; + const first = (p.messages_head + limits.message_log - p.messages_len) % limits.message_log; + return &p.messages[(first + i) % limits.message_log]; + } pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror) void { var buf: [256]u8 = undefined; const text = std.fmt.bufPrint(&buf, "{s}: {s}", .{ operation, @errorName(err) }) catch operation; |
