1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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);
}
|