summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig282
1 files changed, 226 insertions, 56 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 0ac16162..cc57127f 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -39,6 +39,148 @@ pub const font_picker = platform == .gui or platform == .macos;
pub const hosted = platform == .tty or platform == .gui or platform == .macos;
+/// Whose turn it is with the core. The core is single-threaded: the editor's
+/// thread has it, and lets go of it in two kinds of gap -- while it waits for
+/// input (`rest`, `wake`), and while it is out in the host filesystem in the
+/// middle of a step (`yield`, `back`). A 9P connection task takes the turn
+/// in those gaps to answer a request (`take`, `give`), which is what lets the
+/// editor read its own tree through a mount: the syscall goes out, another
+/// thread answers it, the syscall comes back. A step of a connection task's
+/// own -- a Look written to `look` -- goes out the same way.
+///
+/// `out` counts the steps that are out. While one is, the core reads
+/// consistently but that step still holds pointers into it, so nothing may
+/// change it: a request that would is parked in the engine (src/9p_io.zig)
+/// and retried when the turn is next given up with nothing out, which
+/// `parked` remembers to arrange; and the editor's own wake waits for the
+/// count to reach zero, since a step of the editor's may change anything.
+/// It is never a write of its own that a step waits on out there -- writes
+/// come from a shell performing a save between steps -- so a parked request
+/// is never the syscall's own.
+///
+/// One per process, like the editor it belongs to. A process that never
+/// starts it -- a test, the board, the browser -- has no second thread, and
+/// every yield is a no-op.
+pub var turn: Turn = .{};
+
+pub const Turn = struct {
+ mutex: std.Io.Mutex = .init,
+ /// Set by `start`; a process that never starts the turn has none.
+ io: ?std.Io = null,
+ /// Steps out in a host syscall, from any thread.
+ out: u32 = 0,
+ /// A request that changes a pane was parked for want of quiet.
+ parked: bool = false,
+ wake_parked: ?*const fn (?*anyopaque) void = null,
+ wake_ctx: ?*anyopaque = null,
+ /// Bumped each time the editor has performed what the core asked of it,
+ /// so a request that asked for something -- a save, a shell -- can be
+ /// answered once it is done rather than merely queued, the way writing
+ /// `put` to acme's ctl returns after the file is written.
+ epoch: u64 = 0,
+ /// Bumped each time a shell has attempted the Restore the core asked for
+ /// (`takeRestore`), which lives after `pump` because it swaps the core
+ /// out from under it; a request that asked for one waits for this.
+ restores: u64 = 0,
+ /// Both of the above, and `out` reaching zero.
+ settled: std.Io.Condition = .init,
+
+ /// The editor's thread takes the turn for good; it has it until `stop`.
+ pub fn start(t: *Turn, io: std.Io) void {
+ t.io = io;
+ t.mutex.lockUncancelable(io);
+ }
+
+ pub fn stop(t: *Turn) void {
+ const io = t.io orelse return;
+ t.io = null;
+ t.parked = false;
+ t.mutex.unlock(io);
+ }
+
+ /// The editor is about to wait for input: anyone may have the core.
+ pub fn rest(t: *Turn) void {
+ if (t.io == null) return;
+ t.release();
+ }
+
+ /// The editor takes the turn back -- once every step that is out in a
+ /// syscall has returned, since a step of the editor's may change what
+ /// those still point into.
+ pub fn wake(t: *Turn) void {
+ const io = t.io orelse return;
+ t.mutex.lockUncancelable(io);
+ while (t.out != 0) t.settled.waitUncancelable(io, &t.mutex);
+ }
+
+ /// Whoever has the turn is about to block in the host in the middle of
+ /// a step. Nests: a directory listing that resolves each entry yields
+ /// inside a yield, and only the outermost one lets go.
+ pub fn yield(t: *Turn) void {
+ const io = t.io orelse return;
+ yield_depth += 1;
+ if (yield_depth != 1) return;
+ t.out += 1;
+ t.mutex.unlock(io);
+ }
+
+ pub fn back(t: *Turn) void {
+ const io = t.io orelse return;
+ yield_depth -= 1;
+ if (yield_depth != 0) return;
+ t.mutex.lockUncancelable(io);
+ t.out -= 1;
+ if (t.out == 0) t.settled.broadcast(io);
+ }
+
+ /// How many yields this thread is inside of.
+ threadlocal var yield_depth: u32 = 0;
+
+ /// A connection task takes the turn; answers whether nothing is out
+ /// mid-step, which is what a request that changes a pane needs.
+ pub fn take(t: *Turn) bool {
+ t.mutex.lockUncancelable(t.io.?);
+ return t.out == 0;
+ }
+
+ pub fn give(t: *Turn) void {
+ t.release();
+ }
+
+ /// The editor performed the core's effects.
+ pub fn settle(t: *Turn) void {
+ const io = t.io orelse return;
+ t.epoch +%= 1;
+ t.settled.broadcast(io);
+ }
+
+ /// With the turn: waits, turn given up meanwhile, until the editor has
+ /// performed past `epoch`; stopping ends the wait early.
+ pub fn awaitSettled(t: *Turn, epoch: u64) void {
+ while (t.epoch == epoch) t.settled.wait(t.io.?, &t.mutex) catch return;
+ }
+
+ /// The shell attempted the Restore the core asked for, whichever way it
+ /// went; called at the top of each loop step, when the previous step's
+ /// attempt is over.
+ pub fn restoreSettled(t: *Turn) void {
+ const io = t.io orelse return;
+ t.restores +%= 1;
+ t.settled.broadcast(io);
+ }
+
+ pub fn awaitRestored(t: *Turn, restores: u64) void {
+ while (t.restores == restores) t.settled.wait(t.io.?, &t.mutex) catch return;
+ }
+
+ fn release(t: *Turn) void {
+ const woken = t.out == 0 and t.parked;
+ if (woken) t.parked = false;
+ t.mutex.unlock(t.io.?);
+ if (woken) if (t.wake_parked) |f| f(t.wake_ctx);
+ }
+};
+
pub const can_attach = platform == .tty or platform == .gui;
pub const terminal_panes = platform != .esp32p4;
@@ -4754,7 +4896,6 @@ pub const Event = union(enum) {
/// this must not clamp onto and preview the final grid cell.
pointer_leave,
tick,
- fs_req: ctlfs.Req,
};
pub const attach_name_max = 256;
@@ -4792,7 +4933,6 @@ pub const Effect = union(enum) {
/// Write the build-time theme ring below the per-user config directory.
/// The pane receives the completion/error message from the native host.
dump_themes: struct { pane: u8 },
- fs_reply: ctlfs.Reply,
attach: struct { pane: u8, name: Buf(attach_name_max) },
detach: struct { pane: u8 },
quit,
@@ -5910,11 +6050,6 @@ pub const Options = struct {
ninep_name: []const u8 = "",
ninep_tcp: ?[]const u8 = null,
ninep_quic: ?[]const u8 = null,
- ninep_identity: struct {
- socket_path: []const u8 = "",
- tcp_address: ?std.Io.net.IpAddress = null,
- quic_address: ?std.Io.net.IpAddress = null,
- } = .{},
mounts: []const filesystem.Mount = &.{},
config_dir: ?[]const u8 = null,
startup_config_path: ?[]const u8 = null,
@@ -6182,11 +6317,7 @@ pub const Pardes = struct {
.pdf_gpa = pdf_gpa,
.tree_sitter_gpa = tree_sitter_gpa,
.opts = opts,
- .fs = .{
- .socket_path = opts.ninep_identity.socket_path,
- .tcp_address = opts.ninep_identity.tcp_address,
- .quic_address = opts.ninep_identity.quic_address,
- },
+ .fs = .{},
.screen_w = opts.cols,
.screen_h = opts.rows,
.scratch = .init(gpa),
@@ -6197,7 +6328,6 @@ pub const Pardes = struct {
p.fs.started = ctlfs.events.now();
for (opts.mounts) |mount| try p.fs.mount(gpa, mount.name, mount.dial);
p.opts.mounts = &.{};
- p.opts.ninep_identity = .{};
p.boot = Boot.of(opts);
switch (p.boot) {
.document => {
@@ -6310,13 +6440,31 @@ pub const Pardes = struct {
return @intCast(std.math.clamp(panes.File.displayWidth(text) + 2, 1, @as(usize, limit)));
}
+ const Printed = struct { left: u16, dropped: usize };
+
/// Prints `text` flush with the right edge of the band, and answers the
- /// column it started at so a cursor can follow it.
- fn printRight(s: *Surface, x: u16, row: u16, w: u16, text: []const u8, style: CellStyle) u16 {
- const shown: u16 = @intCast(@min(@as(usize, w), panes.File.displayWidth(text)));
- const left = x + w - shown;
- _ = s.print(left, row, shown, text, style);
- return left;
+ /// column it started at so a cursor can follow it. Text wider than the
+ /// band loses its HEAD: the end is the part that says something -- a
+ /// path's file name, an error's reason -- and `dropped` is how many
+ /// columns of it went.
+ fn printRight(s: *Surface, x: u16, row: u16, w: u16, text: []const u8, style: CellStyle) Printed {
+ const width = panes.File.displayWidth(text);
+ if (width <= w) {
+ const left: u16 = @intCast(x + w - width);
+ _ = s.print(left, row, @intCast(width), text, style);
+ return .{ .left = left, .dropped = 0 };
+ }
+ // Whole glyphs only: a cut inside a wide one would leave the rest a
+ // column too wide and clip its LAST glyph instead of the first.
+ var dropped = width - w;
+ var start = panes.File.rawAtDisplay(text, dropped);
+ if (panes.File.rawDisplayCol(text, start) < dropped) {
+ dropped += 1;
+ start = panes.File.rawAtDisplay(text, dropped);
+ }
+ const left: u16 = @intCast(x + w - (width - dropped));
+ _ = s.print(left, row, @intCast(width - dropped), text[start..], style);
+ return .{ .left = left, .dropped = dropped };
}
/// The layout every single-pane boot starts from.
@@ -6904,8 +7052,14 @@ pub const Pardes = struct {
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]);
+ // One row of printable text: a language server's multi-line report
+ // reads as one line, and the cut never leaves half a glyph behind.
+ var n: usize = @min(text.len, pane.msg.len);
+ if (n < text.len) {
+ while (n > 0 and text[n] & 0xC0 == 0x80) n -= 1;
+ }
+ for (text[0..n], pane.msg[0..n]) |c, *cell| cell.* = if (c < 0x20 or c == 0x7f) ' ' else c;
+ pane.msg_len = @intCast(n);
}
fn logMessage(p: *Pardes, id: usize, text: []const u8) void {
@@ -7040,7 +7194,7 @@ pub const Pardes = struct {
pub fn postEvent(p: *Pardes, ev: Event) void {
switch (ev) {
.key => |k| std.debug.assert(k.text.len == 0),
- .output, .paste, .lsp_resp, .pipe_resp, .file_changed, .command, .fs_req => unreachable,
+ .output, .paste, .lsp_resp, .pipe_resp, .file_changed, .command => unreachable,
else => {},
}
if (p.in_len == p.in_q.len) return;
@@ -7173,7 +7327,6 @@ pub const Pardes = struct {
},
.theme_file => |t| if (v.watch_theme) |f| f(p.host.ctx, t.generation, t.on),
.dump_themes => |d| if (v.dump_themes) |f| f(p.host.ctx, d.pane),
- .fs_reply => {},
.attach => |a| {
const name = a.name.slice();
@memcpy(p.attach_buf[0..name.len], name);
@@ -7194,6 +7347,7 @@ pub const Pardes = struct {
if (v.wait_input) |f| f(h.ctx, if (p.animationActive()) layout.Animation.frame_ms else 0);
while (p.nextQueued()) |ev| p.update(ev);
while (p.nextEffect()) |e| p.perform(e);
+ turn.settle();
// A quitting frame has already freed what it would draw.
if (p.quit) return;
if (v.poll_frame) |f| f(h.ctx);
@@ -7208,24 +7362,35 @@ pub const Pardes = struct {
p.needs_frame = false;
}
+ /// One request of the control filesystem, from whichever thread has the
+ /// turn (`pardes.turn`). Not an event: it neither sweeps nor resets
+ /// anything the editor's own step may be in the middle of using, so it
+ /// can be answered while that step is out in a syscall. Only a request
+ /// that changes a pane costs a frame, which is why a round trip on a
+ /// local socket costs microseconds and not a vsync.
+ pub fn serveFs(p: *Pardes, req: ctlfs.Req) ctlfs.Reply {
+ if (!ctlfs.changesPane(req)) return ctlfs.handle(p, req);
+ p.needs_frame = true;
+ p.raw_hover_intent = false;
+ p.cancelLookHover();
+ const reply = ctlfs.handle(p, req);
+ // The request was a whole step of its own, so it settles the way a
+ // step does: the cursor and scroll reconciled, the scripted panes
+ // told, and the panes it made announced to /log now rather than at
+ // the end of whatever the editor does next.
+ p.sync();
+ p.fsReport();
+ ctlfs.events.announce(p);
+ return reply;
+ }
+
pub fn update(p: *Pardes, ev: Event) void {
- // Only two events cannot change the screen: a tick with nothing
- // animating, and a filesystem request that only reads. The second is
- // what 9P traffic overwhelmingly is, and answering it used to drag a
- // whole vsync-blocked frame behind it -- which is why a round trip on
- // a local socket cost a frame instead of a few microseconds.
- p.needs_frame = p.needs_frame or switch (ev) {
- .tick => false,
- .fs_req => |req| ctlfs.changesPane(req),
- else => true,
- };
+ // A tick with nothing animating is the one event that cannot change
+ // the screen.
+ p.needs_frame = p.needs_frame or ev != .tick;
p.shell_rows.sweep(p.gpa);
switch (ev) {
.tick => {},
- .fs_req => |req| if (ctlfs.changesPane(req)) {
- p.raw_hover_intent = false;
- p.cancelLookHover();
- },
.mouse => |m| if (!(m.button == .none and m.kind == .motion)) {
p.raw_hover_intent = false;
p.cancelLookHover();
@@ -7324,7 +7489,6 @@ pub const Pardes = struct {
},
.paste => |bytes| p.applyPaste(bytes),
.command => |line| _ = p.executeBuiltinLine(p.active, line),
- .fs_req => |r| p.emit(.{ .fs_reply = ctlfs.handle(p, r) }),
.pinch => |scale| p.ov_pinch_scale = scale,
.touch_scroll => |delta| p.ov_touch_scroll_delta = delta,
.pointer_leave => {
@@ -12915,11 +13079,7 @@ pub const Pardes = struct {
.pdf_gpa = pdf_gpa,
.tree_sitter_gpa = tree_sitter_gpa,
.opts = opts,
- .fs = .{
- .socket_path = opts.ninep_identity.socket_path,
- .tcp_address = opts.ninep_identity.tcp_address,
- .quic_address = opts.ninep_identity.quic_address,
- },
+ .fs = .{},
.screen_w = opts.cols,
.screen_h = opts.rows,
.scratch = .init(gpa),
@@ -12934,13 +13094,9 @@ pub const Pardes = struct {
p.cell_pixels = old.cell_pixels;
p.row_metrics = old.row_metrics;
p.native_images = old.native_images;
- p.fs.socket_path = old.fs.socket_path;
- p.fs.tcp_address = old.fs.tcp_address;
- p.fs.quic_address = old.fs.quic_address;
}
for (opts.mounts) |mount| try p.fs.mount(gpa, mount.name, mount.dial);
p.opts.mounts = &.{};
- p.opts.ninep_identity = .{};
var parsed = try dump.readZon(gpa, zon_bytes, "load");
defer parsed.deinit();
const st = parsed.value;
@@ -13521,16 +13677,19 @@ pub const Pardes = struct {
// right would put it past the pane, off the grid, and past what
// the detached wire will encode -- which drops every frame for
// as long as the prompt is up.
- const left = printRight(s, cx, row, chip -| 1, text, msg_style);
+ const printed = printRight(s, cx, row, chip -| 1, text, msg_style);
if (kind != .prompt or id != p.active) continue;
const at = pane.promptAt() orelse continue;
const prompt0 = (p.tagPrefix(pane) catch continue).len + at;
const col = @as(usize, pane.tag_col);
if (col < prompt0) continue;
- // The cursor follows the text to wherever it landed.
+ // The cursor follows the text to wherever it landed; a caret
+ // in the part a narrow band dropped has nowhere to be.
const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]);
- if (left + prompt_col < tx + tw)
- s.cursor = .{ .x = left + @as(u16, @intCast(prompt_col)), .y = row, .bar = pane.mode == .insert };
+ if (prompt_col < printed.dropped) continue;
+ const caret = printed.left + (prompt_col - printed.dropped);
+ if (caret < tx + tw)
+ s.cursor = .{ .x = @intCast(caret), .y = row, .bar = pane.mode == .insert };
}
}
@@ -14018,12 +14177,19 @@ pub const Pardes = struct {
const chip = r.x + r.w - cx;
// Right aligned inside the chip, a blank cell short of its
// edge: the same one the grid pass leaves for a prompt caret.
+ // Wider than the chip, the text loses its head, as on the grid.
const room = p.tagCapacity(chip) -| 1;
const shown = panes.File.displayWidth(text);
- const pad = room -| shown;
- const line = try arena.alloc(u8, pad + text.len);
+ var kept = text;
+ if (shown > room) {
+ var start = panes.File.rawAtDisplay(text, shown - room);
+ if (panes.File.rawDisplayCol(text, start) < shown - room) start = panes.File.rawAtDisplay(text, shown - room + 1);
+ kept = text[start..];
+ }
+ const pad = room -| panes.File.displayWidth(kept);
+ const line = try arena.alloc(u8, pad + kept.len);
@memset(line[0..pad], ' ');
- @memcpy(line[pad..], text);
+ @memcpy(line[pad..], kept);
try p.renderHeaderLayer(arena, NOTICE_LAYER_BASE + id * Pane.Notices.max + i, .notice, @intCast(id), .{
.x = cx,
.y = first + @as(u16, @intCast(i)),
@@ -14221,8 +14387,12 @@ pub const Pardes = struct {
if (pane.hasPdf() and panes.Pdf.draw(p, pane, r, id, tx, tw)) return;
if (pane.image) |*iv| {
- const image_h = r.h -| BOX_H;
- panes.Image.draw(p, iv, @intCast(id), pane.serial, tx, body_y, tw, image_h);
+ // Below the notice chips: a picture is drawn after the cells (the
+ // GUI's image pass, kitty's z-order), so a chip over it would be
+ // painted out. Text gets the overlay; a picture gives up the rows.
+ const shown = pane.notices.len;
+ const image_h = (r.h -| BOX_H) -| shown;
+ panes.Image.draw(p, iv, @intCast(id), pane.serial, tx, body_y + shown, tw, image_h);
// thumbless, but the same one column as the real scrollbar below —
// that is the whole point of drawing it, and like that one it runs
// past the notice bands so the gutter has no notch in it