summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig136
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;