diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 282 |
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 |
