diff options
Diffstat (limited to 'src/tty.zig')
| -rw-r--r-- | src/tty.zig | 118 |
1 files changed, 116 insertions, 2 deletions
diff --git a/src/tty.zig b/src/tty.zig index 2326924c..818d8b48 100644 --- a/src/tty.zig +++ b/src/tty.zig @@ -34,10 +34,34 @@ pub const Command = struct { winsize: vaxis.Winsize, mouse: vaxis.Mouse, paste: []const u8, + /// a language query finished on a worker; rows are gpa-owned + lsp_done: struct { id: u32, rows: []u8 }, } = .nop; }; const Loop = vaxis.Loop(@TypeOf(Command.value)); +/// One language query, owned by the worker that runs it. Everything the +/// backend may read is copied in here before the worker starts: the core goes +/// on editing the moment the effect is drained, so a borrowed slice would be a +/// use-after-free the length of one keystroke. +const LspJob = struct { + id: u32, + kind: pardes.lsp.Kind, + offset: u32, + path: []u8, + source: [:0]u8, + arg: []u8, + root: []u8, + + fn free(j: *LspJob, gpa: std.mem.Allocator) void { + gpa.free(j.path); + gpa.free(j.source); + gpa.free(j.arg); + gpa.free(j.root); + gpa.destroy(j); + } +}; + const Pty = struct { file: std.Io.File, pid: posix.pid_t, @@ -101,6 +125,8 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { // per-slot spawn generation: a reused pane id ignores the old shell's // late pty_eof (which would otherwise close the NEW pty on that slot) var gens: [pardes.MAX_PANES]u32 = @splat(0); + // the single in-flight language query (see the .lsp effect) + var lsp_task: ?std.Io.Future(anyerror!void) = null; var loop: Loop = .init(io, &tty, &vx); defer { // reap the reader tasks (cancel interrupts a blocked read) before @@ -112,16 +138,23 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { _ = libc.close(pt.file.handle); slot.* = null; }; + // join the query worker BEFORE the drain below, or its late post + // lands in a queue nobody empties again and the rows leak + if (lsp_task) |*t| { + t.cancel(io) catch {}; + lsp_task = null; + } while (loop.tryEvent() catch null) |ev| switch (ev) { .pty_read => |pr| gpa.free(pr.bytes), .paste => |b| gpa.free(@constCast(b)), + .lsp_done => |d| gpa.free(d.rows), else => {}, }; } // Perform the initial spawns BEFORE any worker thread exists: forkpty from // a multithreaded process can wedge the child before exec. - drainEffects(core, &ptys, &gens, io, gpa, &loop, &vx, &tty, false); + drainEffects(core, &ptys, &gens, io, gpa, &loop, &vx, &tty, &lsp_task, false); try loop.start(); defer loop.stop(); @@ -205,6 +238,16 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { core.update(.{ .paste = bytes }); gpa.free(@constCast(bytes)); }, + .lsp_done => |d| { + core.update(.{ .lsp_resp = .{ .id = d.id, .rows = d.rows } }); + gpa.free(d.rows); + // the worker is finished; join it so its future does not + // leak (same contract as pty_eof above) + if (lsp_task) |*t| { + t.await(io) catch {}; + lsp_task = null; + } + }, } batch += 1; if (stop or output or batch >= 64) break; @@ -213,7 +256,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { tz_event.end(); if (stop) break; - drainEffects(core, &ptys, &gens, io, gpa, &loop, &vx, &tty, true); + drainEffects(core, &ptys, &gens, io, gpa, &loop, &vx, &tty, &lsp_task, true); if (core.quit) break; // Restore builtin: swap in a core rebuilt from the dump; the live @@ -302,6 +345,7 @@ fn drainEffects( loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty, + lsp_task: *?std.Io.Future(anyerror!void), threads_ok: bool, ) void { while (core.nextEffect()) |effect| switch (effect) { @@ -370,10 +414,80 @@ fn drainEffects( // mirror the core's yank register out via OSC 52 if (core.yank) |y| vx.copyToSystemClipboard(tty.writer(), y, gpa) catch {}; }, + .lsp => |q| { + if (!threads_ok) continue; // pre-loop drain: nothing to answer to yet + const pane = core.panes[q.pane] orelse continue; + const f = pane.file orelse continue; + // snapshot everything the worker may read (see LspJob) + const job = gpa.create(LspJob) catch continue; + job.* = .{ + .id = q.id, + .kind = q.kind, + .offset = q.offset, + .path = gpa.dupe(u8, f.path) catch { + gpa.destroy(job); + continue; + }, + .source = gpa.dupeZ(u8, f.content) catch { + gpa.free(job.path); + gpa.destroy(job); + continue; + }, + .arg = gpa.dupe(u8, q.arg.slice()) catch { + gpa.free(job.path); + gpa.free(job.source); + gpa.destroy(job); + continue; + }, + .root = gpa.dupe(u8, std.fs.path.dirname(f.path) orelse "/") catch { + gpa.free(job.path); + gpa.free(job.source); + gpa.free(job.arg); + gpa.destroy(job); + continue; + }, + }; + // ponytail: ONE query in flight, so one future slot. Replacing it + // cancels-then-joins the previous worker, which for a backend that + // ignores cancellation means waiting out a query the user already + // abandoned. Queries are milliseconds; make this a real pool the + // day a backend takes long enough to notice. + if (lsp_task.*) |*old| { + old.cancel(io) catch {}; + lsp_task.* = null; + } + lsp_task.* = io.concurrent(lspWorker, .{ gpa, job, loop }) catch { + job.free(gpa); + continue; + }; + }, .quit => {}, }; } +/// Answer a language query off the event loop and post the rows back. This is +/// the whole async execution model: the same shape as readPty — do the slow +/// thing on a worker, hand the result to the loop as an event, let the core +/// stay a state machine that never blocks. +fn lspWorker(gpa: std.mem.Allocator, job: *LspJob, loop: *Loop) anyerror!void { + defer job.free(gpa); + var arena: std.heap.ArenaAllocator = .init(gpa); + defer arena.deinit(); + var out: std.ArrayList(u8) = .empty; + defer out.deinit(gpa); + pardes.lsp.query(gpa, arena.allocator(), .{ + .kind = job.kind, + .path = job.path, + .source = job.source, + .offset = job.offset, + .arg = job.arg, + .root = job.root, + }, &out); + const rows = gpa.dupe(u8, out.items) catch return; + loop.postEvent(.{ .lsp_done = .{ .id = job.id, .rows = rows } }) catch gpa.free(rows); + return; +} + /// Consume SIGWINCH synchronously (it is blocked in every thread) and post /// the new size as a winsize event from normal thread context — the one place /// vaxis's Io-backed queue is safe to touch on a resize. |
