summaryrefslogtreecommitdiff
path: root/src/lsp_host.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/lsp_host.zig')
-rw-r--r--src/lsp_host.zig206
1 files changed, 206 insertions, 0 deletions
diff --git a/src/lsp_host.zig b/src/lsp_host.zig
new file mode 100644
index 00000000..803beb31
--- /dev/null
+++ b/src/lsp_host.zig
@@ -0,0 +1,206 @@
+//! Turning the core's `lsp` effect into work on a thread, and its rows back
+//! into something a loop can deliver. Native-shell side, like host_io.zig and
+//! shell_bin.zig, and here for the reason those are: all three native shells
+//! need it and none of them needs a different one.
+//!
+//! It was two copies before it was this. tty.zig had it inline and gui.zig had
+//! it again with `tty.zig's LspJob, and copied for the same reason` written
+//! over the top — identical down to the comment about a pane with no file. The
+//! AppKit shell had NEITHER, so its vtable left `pull_lsp` null, the core
+//! answered its own requests with no rows, and every language query did nothing
+//! at all in the shell most people run. None of that looked like a missing
+//! feature from the outside: `gd` just moved no cursor. A third copy is what
+//! this module exists instead of.
+//!
+//! What is genuinely per-host stays per-host, and it is small: which allocator,
+//! how a finished job reaches the loop (a mutex queue, a vaxis event, an inbox
+//! plus a wakeup), and what bounds the in-flight set (a refcount to join at
+//! teardown, or one future to cancel). What is NOT per-host is everything
+//! below: the core goes on editing the moment the effect is drained, so every
+//! byte the backend may read has to be COPIED first, and getting that ladder
+//! subtly different in three places is how one shell reads freed text one
+//! keystroke later.
+const std = @import("std");
+const pardes = @import("pardes.zig");
+const host_api = @import("host.zig");
+
+/// One language query, owned by the worker that runs it.
+pub const Job = struct {
+ id: u32,
+ kind: pardes.lsp.Kind,
+ offset: u32,
+ path: []u8,
+ source: [:0]u8,
+ arg: []u8,
+ root: []u8,
+
+ pub fn free(job: *Job, gpa: std.mem.Allocator) void {
+ gpa.free(job.path);
+ gpa.free(job.source);
+ gpa.free(job.arg);
+ gpa.free(job.root);
+ gpa.destroy(job);
+ }
+};
+
+/// Copy the query out of the core. Null when the pane is gone or an allocation
+/// failed, and nothing leaks on either path — the ladder frees exactly what it
+/// had managed to take.
+///
+/// A pane with no file still asks `status`: that query is about the BACKEND,
+/// not the buffer. Empty path and source then, and the root comes off the
+/// pane's cwd so a bare terminal still reports which servers it would reach.
+pub fn snapshot(gpa: std.mem.Allocator, core: *const pardes.Pardes, req: host_api.LspRequest) ?*Job {
+ const pane = core.panes[req.pane] orelse return null;
+ const file = pane.file;
+ const job = gpa.create(Job) catch return null;
+ job.* = .{
+ .id = req.id,
+ .kind = req.kind,
+ .offset = req.offset,
+ .path = gpa.dupe(u8, if (file) |f| f.path else "") catch {
+ gpa.destroy(job);
+ return null;
+ },
+ .source = gpa.dupeZ(u8, if (file) |f| f.content else "") catch {
+ gpa.free(job.path);
+ gpa.destroy(job);
+ return null;
+ },
+ .arg = gpa.dupe(u8, req.arg) catch {
+ gpa.free(job.path);
+ gpa.free(job.source);
+ gpa.destroy(job);
+ return null;
+ },
+ .root = gpa.dupe(u8, if (file) |f|
+ (std.fs.path.dirname(f.path) orelse "/")
+ else
+ pane.cwdSlice()) catch {
+ gpa.free(job.path);
+ gpa.free(job.source);
+ gpa.free(job.arg);
+ gpa.destroy(job);
+ return null;
+ },
+ };
+ return job;
+}
+
+/// What a host does with finished rows. It TAKES OWNERSHIP of `rows`, which
+/// were allocated with the same allocator the job was.
+pub const Deliver = *const fn (ctx: ?*anyopaque, id: u32, rows: []u8) void;
+
+/// Run `job` to completion and hand its rows to `deliver`. Consumes the job
+/// either way.
+///
+/// This is the whole async execution model, and it is the one every shell
+/// already uses for its pty reader: do the slow thing off the loop, hand the
+/// result over as an event, let the core stay a state machine that never
+/// blocks. The shell owns the result buffer; the backend only writes into it.
+pub fn work(gpa: std.mem.Allocator, job: *Job, ctx: ?*anyopaque, deliver: Deliver) void {
+ defer job.free(gpa);
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ defer out.deinit();
+ 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.writer);
+ // Duped out of the writer: `deliver` outlives this frame and the writer
+ // does not. A failed dupe drops the answer, which the core survives — the
+ // request times out into no rows, exactly as an empty answer would.
+ const rows = gpa.dupe(u8, out.written()) catch return;
+ deliver(ctx, job.id, rows);
+}
+
+test "a snapshot owns every byte the backend will read" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .tty_only = true });
+ defer core.deinit();
+
+ var needle: [6]u8 = "needle".*;
+ const job = snapshot(gpa, core, .{
+ .id = 7,
+ .kind = .status,
+ .pane = @intCast(core.active),
+ .offset = 0,
+ .arg = &needle,
+ }) orelse return error.SnapshotFailed;
+ defer job.free(gpa);
+
+ try std.testing.expectEqual(@as(u32, 7), job.id);
+ try std.testing.expectEqual(pardes.lsp.Kind.status, job.kind);
+ // `arg` is the caller's buffer on the way in and the job's own bytes on the
+ // way out. THIS is the property the whole ladder exists for: the core reuses
+ // that buffer for the next builtin's argument the moment the effect drains.
+ try std.testing.expectEqualStrings("needle", job.arg);
+ try std.testing.expect(job.arg.ptr != &needle);
+ // A terminal pane has no file and the query still has to be answerable:
+ // empty path, a NUL-terminated empty source, and the pane's own cwd as the
+ // root so a bare terminal still reports which servers it would reach. The
+ // cwd may legitimately be empty in a core that has never spawned a shell;
+ // what matters is that the job OWNS it rather than borrowing it.
+ try std.testing.expectEqualStrings("", job.path);
+ try std.testing.expectEqual(@as(usize, 0), job.source.len);
+ try std.testing.expectEqual(@as(u8, 0), job.source[0]);
+ const pane = core.panes[core.active].?;
+ try std.testing.expectEqualStrings(pane.cwdSlice(), job.root);
+ if (job.root.len > 0) try std.testing.expect(job.root.ptr != pane.cwdSlice().ptr);
+}
+
+test "a pane that is gone yields no job rather than a null deref" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .tty_only = true });
+ defer core.deinit();
+
+ // The effect is drained after the core has moved on, so the pane it names
+ // may already have been deleted. Every shell open-coded this check.
+ const empty = for (core.panes, 0..) |slot, id| {
+ if (slot == null) break @as(u8, @intCast(id));
+ } else return error.NoEmptyPane;
+ try std.testing.expect(snapshot(gpa, core, .{
+ .id = 1,
+ .kind = .definition,
+ .pane = empty,
+ .offset = 0,
+ .arg = "",
+ }) == null);
+}
+
+test "work consumes the job and hands its rows to the sink" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .tty_only = true });
+ defer core.deinit();
+
+ const Sink = struct {
+ var seen_id: u32 = 0;
+ var seen_rows: ?[]u8 = null;
+ fn take(_: ?*anyopaque, id: u32, rows: []u8) void {
+ seen_id = id;
+ seen_rows = rows;
+ }
+ };
+ Sink.seen_id = 0;
+ Sink.seen_rows = null;
+
+ const job = snapshot(gpa, core, .{
+ .id = 42,
+ .kind = .status,
+ .pane = @intCast(core.active),
+ .offset = 0,
+ .arg = "",
+ }) orelse return error.SnapshotFailed;
+ work(gpa, job, null, Sink.take);
+
+ // `status` is the one kind that answers with no file and no cursor, which
+ // is what makes it assertable here without a language server on the box.
+ try std.testing.expectEqual(@as(u32, 42), Sink.seen_id);
+ const rows = Sink.seen_rows orelse return error.SinkNeverCalled;
+ defer gpa.free(rows);
+}