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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
|
//! ZLS as a LIBRARY, not a language server.
//!
//! There is no subprocess, no JSON-RPC, no `initialize` handshake and no
//! `Server`. `gd` runs ZLS's own analyser on this thread and comes back with a
//! token; the whole "protocol" is a struct field. ZLS's build.zig already
//! publishes its guts as an importable module (`b.modules.put("zls", ...)`),
//! and everything under `features/` that is not welded to `*Server` is a plain
//! function over (analyser, arena, handle, offset) — so the parts we want are
//! callable and the parts we don't want cost nothing.
//!
//! What that buys and what it costs:
//!
//! - Cold IS warm. There is no server to start, no workspace to index, no
//! handshake to wait for. The first `gd` after launch pays for parsing this
//! file and whatever it imports, and nothing else.
//! - It also means NOTHING IS CACHED between queries. Every call builds a
//! DocumentStore, resolves imports, and throws it all away, because `query`
//! is handed an arena that dies on return and a `req` that is a snapshot of
//! a buffer the user is still typing into. A cache would have to be a global
//! with its own mutex and its own invalidation story; that is a real design,
//! not a line of code, and it is the obvious next step rather than something
//! smuggled in here.
//! - `zig_exe_path` is null on purpose: shelling out to the compiler is the
//! thing this backend exists to avoid. `zig_lib_dir` is the build's own
//! (`b.graph.zig_lib_directory`), which is what makes `std` resolvable —
//! `gd` on `std.mem.count` lands in the real `mem.zig`. The price is that
//! `@import("builtin")`, `@import("<pkg>")` and `@cImport` resolve to
//! nothing, silently, because those need a build graph we refuse to run.
const std = @import("std");
const zls = @import("zls");
const lsp = @import("lsp.zig");
const cfg = @import("pardes_config");
const Analyser = zls.Analyser;
const DocumentStore = zls.DocumentStore;
const InternPool = zls.analyser.InternPool;
const offsets = zls.offsets;
const Uri = zls.Uri;
const types = zls.lsp.types;
const Ast = std.zig.Ast;
/// BYTE offsets. ZLS defaults to utf-16 because that is what the wire protocol
/// says; we are not on a wire, and `+Search` rows are byte columns.
const enc: offsets.Encoding = .@"utf-8";
/// Guards on the workspace walks (workspace symbols / workspace diagnostics).
/// A language query runs on every keypress of `SPC S`; it may not turn into an
/// unbounded tree walk because someone opened a file in `/`.
const max_files = 512;
const max_rows = 2000;
pub const supports: std.EnumSet(lsp.Kind) = .initMany(&.{
.definition,
.declaration,
.type_definition,
.implementation,
.references,
.select_refs,
.hover,
.document_symbols,
.workspace_symbols,
.diagnostics,
.workspace_diagnostics,
.rename,
.format,
.code_action,
});
/// The seam's contract: never fail, never panic, no rows is a legal answer.
/// Every error path in here — OOM, a cancelled io, a file that vanished —
/// collapses to "appended nothing", which the core already treats as "no
/// result". There is deliberately no error rendering.
pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.ArrayList(u8)) void {
run(gpa, arena, req, out) catch {};
}
fn run(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.ArrayList(u8)) !void {
// The blocking, single-threaded std.Io — the same one look.zig walks
// directories with. ZLS wants an `Io` for every file read; a worker thread
// that is allowed to block wants the one that just does the syscall.
const io = std.Io.Threaded.global_single_threaded.io();
// The two workspace kinds are about the PROJECT, not about this buffer, so
// they answer from any pane and never need the analyser, the store or the
// intern pool — a walk and a parse each.
switch (req.kind) {
.workspace_symbols => return workspaceSymbols(gpa, arena, io, req, out),
.workspace_diagnostics => return workspaceDiagnostics(gpa, arena, io, req, out),
// Everything else is about THIS file, and this backend speaks Zig. The
// core does not gate the keymap by file type (a pane is a pane), so
// the gate is here: `gd` in a README must find nothing rather than
// parse prose as Zig and confidently resolve a word out of it.
else => if (!std.mem.endsWith(u8, req.path, ".zig")) return,
}
// Pure tree, no analyser.
if (req.kind == .format) return formatQuery(gpa, arena, req, out);
var env: std.process.Environ.Map = .init(arena);
var lib_dir: ?std.Build.Cache.Directory = null;
if (zigLibPath()) |p| {
if (std.Io.Dir.cwd().openDir(io, p, .{})) |h| {
lib_dir = .{ .handle = h, .path = p };
} else |_| {}
}
defer if (lib_dir) |d| d.handle.close(io);
var ip: InternPool = try .init(io, gpa);
defer ip.deinit(gpa);
var diags: zls.DiagnosticsCollection = .{ .io = io, .allocator = gpa };
defer diags.deinit();
var store: DocumentStore = .{
.io = io,
.allocator = gpa,
.config = .{
.environ_map = &env,
// no compiler subprocess: that is the entire point of this backend
.zig_exe_path = null,
.zig_lib_dir = lib_dir,
.build_runner_path = null,
.builtin_path = null,
.global_cache_dir = null,
.wasi_preopens = {},
},
.diagnostics_collection = &diags,
};
defer store.deinit();
// The buffer the user is looking at, not the file on disk: `source` is the
// shell's snapshot, so unsaved edits are what gets analysed.
const uri: Uri = try .fromPath(arena, req.path);
try store.openLspSyncedDocument(uri, req.source);
const handle = store.getHandle(uri) orelse return;
if (handle.tree.mode == .zon) return;
var analyser: Analyser = .init(gpa, arena, &store, &ip, handle);
defer analyser.deinit();
const off: usize = @min(req.offset, req.source.len);
switch (req.kind) {
.definition, .implementation => try goto(gpa, arena, &analyser, handle, off, .definition, out),
.declaration => try goto(gpa, arena, &analyser, handle, off, .declaration, out),
.type_definition => try goto(gpa, arena, &analyser, handle, off, .type_definition, out),
.hover => try hover(gpa, arena, &analyser, handle, off, out),
.document_symbols => try documentSymbols(gpa, arena, req.path, &handle.tree, out),
.references, .select_refs => try references(gpa, arena, &analyser, handle, off, null, out),
.rename => try references(gpa, arena, &analyser, handle, off, req.arg, out),
.diagnostics => try diagnostics(gpa, arena, req.path, &handle.tree, out),
.code_action => try codeActions(gpa, arena, &analyser, handle, off, out),
else => {},
}
}
/// The zig lib directory, which is the one thing this backend cannot work out
/// for itself: it is baked in at build time from `b.graph.zig_lib_directory`,
/// i.e. the exact stdlib pardes was compiled against, and ZIG_LIB_DIR wins if
/// the user moved it. Without this, `gd` on anything in `std` finds nothing.
fn zigLibPath() ?[]const u8 {
if (std.c.getenv("ZIG_LIB_DIR")) |p| {
const s = std.mem.span(p);
if (s.len != 0) return s;
}
if (cfg.zig_lib_dir.len != 0) return cfg.zig_lib_dir;
return null;
}
// ---------------------------------------------------------------- rows
/// Byte offset -> line, without rescanning the file per row. Built once per
/// query over the file we emit many rows from (symbols, references,
/// diagnostics); a goto that emits two rows just scans.
const Lines = struct {
starts: []const u32,
src: []const u8,
fn build(arena: std.mem.Allocator, src: []const u8) !Lines {
var starts: std.ArrayList(u32) = .empty;
try starts.append(arena, 0);
for (src, 0..) |c, i| if (c == '\n') try starts.append(arena, @intCast(i + 1));
return .{ .starts = starts.items, .src = src };
}
fn line(l: Lines, n: usize) []const u8 {
if (n >= l.starts.len) return "";
const a = l.starts[n];
const b = if (n + 1 < l.starts.len) l.starts[n + 1] - 1 else l.src.len;
return l.src[a..@min(b, l.src.len)];
}
};
/// The source line at a 0-based line number, scanned. For the one-or-two-row
/// answers (a goto lands in a file we opened only to read one line out of).
fn lineAt(src: []const u8, want: usize) []const u8 {
var it = std.mem.splitScalar(u8, src, '\n');
var i: usize = 0;
while (it.next()) |l| : (i += 1) if (i == want) return l;
return "";
}
/// Emit one row for a token in whatever file it lives in — which for a goto
/// into `std` is not the file the cursor was in, so the path comes off the
/// handle's uri rather than off `req`.
fn rowForToken(gpa: std.mem.Allocator, arena: std.mem.Allocator, th: Analyser.TokenWithHandle, out: *std.ArrayList(u8)) !void {
const tree = &th.handle.tree;
if (th.token >= tree.tokens.len) return;
const r = offsets.tokenToRange(tree, th.token, enc);
const path = th.handle.uri.toFsPath(arena) catch return;
lsp.row(gpa, out, path, r.start.line, r.start.character, lineAt(tree.source, r.start.line));
}
// ---------------------------------------------------------------- goto
const GotoKind = enum { declaration, definition, type_definition };
/// `goto.zig:gotoHandler` minus the Server, minus the LSP types, minus the
/// linkSupport branch: dispatch on the position context, resolve to a decl,
/// resolve the decl to a token, print the token's line. The five helix gotos
/// collapse onto three ZLS kinds (`gi` behaves as `gd`, which is what ZLS's
/// own implementation handler does too).
fn goto(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
kind: GotoKind,
out: *std.ArrayList(u8),
) !void {
const tree = &handle.tree;
const ctx = try Analyser.getPositionContext(arena, tree, off, true);
var decls: std.ArrayList(Analyser.DeclWithHandle) = .empty;
switch (ctx) {
.var_access, .test_doctest_name => {
const name_loc = offsets.identifierLocFromIndex(tree, off) orelse return;
const name = offsets.locToSlice(tree.source, name_loc);
if (std.mem.eql(u8, name, "_")) return;
if (std.zig.isPrimitive(name)) return;
const d = try analyser.lookupSymbolGlobal(handle, name, off) orelse return;
try decls.append(arena, d);
},
.field_access => |loc| {
const name_loc = offsets.identifierLocFromIndex(tree, off) orelse return;
const name = offsets.locToSlice(tree.source, name_loc);
const held = offsets.locMerge(loc, name_loc);
const found = try analyser.getSymbolFieldAccesses(arena, handle, off, held, name) orelse return;
try decls.appendSlice(arena, found);
},
.label_access, .label_decl => {
const name_loc = offsets.identifierLocFromIndex(tree, off) orelse return;
const name = offsets.locToSlice(tree.source, name_loc);
const d = try Analyser.lookupLabel(handle, name, off) orelse return;
try decls.append(arena, d);
},
.enum_literal => {
const name_loc = offsets.identifierLocFromIndex(tree, off) orelse return;
const name = offsets.locToSlice(tree.source, name_loc);
const d = try analyser.getSymbolEnumLiteral(handle, off, name) orelse return;
try decls.append(arena, d);
},
// `gd` on `@import("foo.zig")` opens foo.zig. Only relative imports
// resolve — a package name needs the build graph we do not run.
.import_string_literal, .embedfile_string_literal => {
const loc = ctx.stringLiteralContentLoc(tree.source);
if (loc.start == loc.end) return;
const str = offsets.locToSlice(tree.source, loc);
const res = try analyser.store.uriFromImportStr(arena, handle, str);
switch (res) {
.none => return,
.one => |u| lsp.row(gpa, out, u.toFsPath(arena) catch return, 0, 0, str),
.many => |us| for (us) |u| lsp.row(gpa, out, u.toFsPath(arena) catch continue, 0, 0, str),
}
return;
},
else => return,
}
// One decl that fails to resolve must not swallow the others: a field
// access can name several and a partial answer beats none.
for (decls.items) |d| {
switch (kind) {
.declaration => try rowForToken(gpa, arena, d.definitionToken(analyser, false) catch continue, out),
.definition => try rowForToken(gpa, arena, d.definitionToken(analyser, true) catch continue, out),
.type_definition => {
// peel error unions / pointers / optionals the way ZLS does,
// then take the container's own name token
if (try d.resolveType(analyser)) |ty| {
var r = ty;
while (true) {
r = try analyser.resolveUnwrapErrorUnionType(r, .payload) orelse
try analyser.resolveDerefType(r) orelse
try analyser.resolveOptionalUnwrap(r) orelse break;
}
if (r.typeDefinitionToken()) |th| {
try rowForToken(gpa, arena, th, out);
continue;
}
}
const nd = try d.typeDeclarationNode() orelse continue;
const t2 = &nd.handle.tree;
const rr = offsets.nodeToRange(t2, nd.node, enc);
const path = nd.handle.uri.toFsPath(arena) catch continue;
lsp.row(gpa, out, path, rr.start.line, rr.start.character, lineAt(t2.source, rr.start.line));
},
}
}
}
// ---------------------------------------------------------------- hover
/// `hover.hover` is already Server-free, so this is a call and a reformat: the
/// `+Hover` buffer holds prose, not locations, so the markdown comes through
/// as lines and nothing arms the n/N stepper. Fenced-code markers are dropped
/// because a plain-text pane has nothing to do with them.
fn hover(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
out: *std.ArrayList(u8),
) !void {
const h = try zls.hover.hover(analyser, arena, handle, off, .markdown, enc) orelse return;
const text = switch (h.contents) {
.markup_content => |m| m.value,
else => return,
};
var it = std.mem.splitScalar(u8, text, '\n');
while (it.next()) |ln| {
if (std.mem.startsWith(u8, ln, "```")) continue;
out.print(gpa, "{s}\n", .{std.mem.trimEnd(u8, ln, " \t\r")}) catch return;
}
}
// ---------------------------------------------------------------- symbols
/// `document_symbol.getDocumentSymbols` needs an arena, a tree and an encoding
/// — no store, no analyser, no intern pool. That makes `SPC s` the cheapest
/// thing in this file: a parse and a walk.
fn documentSymbols(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
path: []const u8,
tree: *const Ast,
out: *std.ArrayList(u8),
) !void {
const syms = try zls.document_symbol.getDocumentSymbols(arena, tree, enc);
var n: usize = 0;
try emitSymbols(gpa, arena, path, syms, "", &n, out);
}
/// Depth-first so the rows read in source order, with the parent's name as a
/// qualifier — `Kind.jumpsWhenSingle` rather than a bare `jumpsWhenSingle`,
/// because a flat list of method names is not navigable.
fn emitSymbols(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
path: []const u8,
syms: []const types.DocumentSymbol,
prefix: []const u8,
n: *usize,
out: *std.ArrayList(u8),
) !void {
for (syms) |s| {
if (n.* >= max_rows) return;
n.* += 1;
const name = if (prefix.len == 0)
s.name
else
try std.fmt.allocPrint(arena, "{s}.{s}", .{ prefix, s.name });
const text = if (s.detail) |d|
try std.fmt.allocPrint(arena, "{s} {s}", .{ name, d })
else
name;
lsp.row(gpa, out, path, s.selectionRange.start.line, s.selectionRange.start.character, text);
if (s.children) |kids| try emitSymbols(gpa, arena, path, kids, name, n, out);
}
}
/// `SPC S`: every `.zig` under the pane's directory, parsed, walked, filtered
/// by substring. ZLS's own workspace symbols would only see files already in
/// the DocumentStore (which is one), so this walks the tree itself — and
/// because `getDocumentSymbols` needs nothing but a tree, it never has to open
/// a store or resolve an import to do it.
fn workspaceSymbols(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
io: std.Io,
req: lsp.Req,
out: *std.ArrayList(u8),
) !void {
if (req.arg.len == 0) return;
var files: std.ArrayList([]const u8) = .empty;
try collectZigFiles(arena, io, req.root, &files);
var n: usize = 0;
for (files.items) |path| {
if (n >= max_rows) return;
const src = readFileZ(arena, io, path) catch continue;
var tree: Ast = Ast.parse(arena, src, .zig) catch continue;
defer tree.deinit(arena);
const syms = zls.document_symbol.getDocumentSymbols(arena, &tree, enc) catch continue;
try filterSymbols(gpa, arena, path, syms, "", req.arg, &n, out);
}
}
fn filterSymbols(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
path: []const u8,
syms: []const types.DocumentSymbol,
prefix: []const u8,
needle: []const u8,
n: *usize,
out: *std.ArrayList(u8),
) !void {
for (syms) |s| {
if (n.* >= max_rows) return;
const name = if (prefix.len == 0)
s.name
else
try std.fmt.allocPrint(arena, "{s}.{s}", .{ prefix, s.name });
if (containsIgnoreCase(s.name, needle)) {
n.* += 1;
lsp.row(gpa, out, path, s.selectionRange.start.line, s.selectionRange.start.character, name);
}
if (s.children) |kids| try filterSymbols(gpa, arena, path, kids, name, needle, n, out);
}
}
fn containsIgnoreCase(hay: []const u8, needle: []const u8) bool {
if (needle.len == 0) return true;
if (needle.len > hay.len) return false;
var i: usize = 0;
outer: while (i + needle.len <= hay.len) : (i += 1) {
for (needle, 0..) |c, j|
if (std.ascii.toLower(hay[i + j]) != std.ascii.toLower(c)) continue :outer;
return true;
}
return false;
}
// ------------------------------------------------------------- references
/// `gr` / `SPC h` / `SPC r`. ZLS's `symbolReferences` is private and its
/// `referencesHandler` takes a `*Server`, so this is the same algorithm from
/// the outside: find the decl under the cursor, then offer every identifier
/// token in the file that spells the same name back to the analyser and keep
/// the ones that resolve to the same decl. Resolving rather than matching is
/// what makes a shadowed local not a reference to the outer one.
///
/// LIMIT: this file only. A workspace-wide answer means loading every project
/// file into the store and running the analyser over each — seconds, not
/// milliseconds, on every keypress, and the store's own workspace iteration
/// has the same restriction (it can only see handles that were loaded).
///
/// `new_name` non-null makes it a rename PREVIEW: the same rows, annotated
/// with the replacement. The seam returns rows, not edits, so `SPC r` shows
/// what would change and changes nothing — an honest half of rename.
fn references(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
new_name: ?[]const u8,
out: *std.ArrayList(u8),
) !void {
const tree = &handle.tree;
const target = try declAt(arena, analyser, handle, off) orelse return;
// The decl's name token indexes ITS OWN file, which after an alias resolve
// is not necessarily this one — and on a half-typed file it is not
// necessarily an identifier either, which `identifierTokenToNameSlice`
// asserts. Both of those are crashes on a keystroke, so both are checked.
const decl_tree = &target.handle.tree;
const name_tok = target.nameToken();
if (name_tok >= decl_tree.tokens.len) return;
if (decl_tree.tokenTag(name_tok) != .identifier) return;
const want = offsets.identifierTokenToNameSlice(decl_tree, name_tok);
if (want.len == 0) return;
const lines: Lines = try .build(arena, tree.source);
var n: usize = 0;
for (0..tree.tokens.len) |i| {
if (n >= max_rows) return;
const tok: Ast.TokenIndex = @intCast(i);
if (tree.tokenTag(tok) != .identifier) continue;
if (!std.mem.eql(u8, offsets.identifierTokenToNameSlice(tree, tok), want)) continue;
const at = tree.tokenStart(tok);
const d = (declAt(arena, analyser, handle, at) catch continue) orelse continue;
if (!d.eql(target)) continue;
n += 1;
const r = offsets.tokenToRange(tree, tok, enc);
const text = if (new_name) |nn|
try std.fmt.allocPrint(arena, "{s} -> {s} {s}", .{ want, nn, std.mem.trim(u8, lines.line(r.start.line), " \t") })
else
lines.line(r.start.line);
lsp.row(gpa, out, handle.uri.toFsPath(arena) catch return, r.start.line, r.start.character, text);
}
}
/// The decl under a byte offset, whatever the surrounding syntax is. Shared by
/// references and by rename, which is references with a label on it.
fn declAt(
arena: std.mem.Allocator,
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
) !?Analyser.DeclWithHandle {
const tree = &handle.tree;
const ctx = try Analyser.getPositionContext(arena, tree, off, true);
const name_loc = offsets.identifierLocFromIndex(tree, off) orelse return null;
const name = offsets.locToSlice(tree.source, name_loc);
const d = switch (ctx) {
.var_access, .test_doctest_name => try analyser.lookupSymbolGlobal(handle, name, off),
.field_access => |loc| blk: {
const found = try analyser.getSymbolFieldAccesses(arena, handle, off, offsets.locMerge(loc, name_loc), name) orelse break :blk null;
break :blk if (found.len != 0) found[0] else null;
},
.label_access, .label_decl => try Analyser.lookupLabel(handle, name, off),
.enum_literal => try analyser.getSymbolEnumLiteral(handle, off, name),
else => null,
} orelse return null;
return try analyser.resolveVarDeclAlias(d) orelse d;
}
// ------------------------------------------------------------ diagnostics
/// `SPC d` and the list `]d`/`[d` step. ZLS's `getAstCheckDiagnostics` prefers
/// spawning `zig ast-check`; its OTHER branch runs `std.zig.AstGen` in this
/// process, which is the branch a subprocess-free backend wants, so that is
/// what is inlined here (it is ten lines and needs a `*Server` only for the
/// config it reads).
///
/// A clean file emits ONE row saying so. Silence already means "no backend" in
/// this seam — `lspResponse` opens nothing for zero rows — so a `SPC d` that
/// checked and found nothing has to be able to say the difference.
fn diagnostics(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
path: []const u8,
tree: *const Ast,
out: *std.ArrayList(u8),
) !void {
const n = try treeDiagnostics(gpa, arena, path, tree, out);
if (n == 0) lsp.row(gpa, out, path, 0, 0, "no diagnostics");
}
/// `zig ast-check`, in this process. ZLS spawns the compiler for this when it
/// has one; its fallback branch is `std.zig.AstGen` directly, and that fallback
/// is this backend's only branch. Caller owns the bundle.
fn astCheck(gpa: std.mem.Allocator, path: []const u8, tree: *const Ast) !std.zig.ErrorBundle {
if (tree.errors.len != 0) return .empty;
var zir = std.zig.AstGen.generate(gpa, tree.*) catch return .empty;
defer zir.deinit(gpa);
if (!zir.hasCompileErrors()) return .empty;
var wip: std.zig.ErrorBundle.Wip = undefined;
try wip.init(gpa);
defer wip.deinit();
wip.addZirErrorMessages(zir, tree.*, tree.source, path) catch return .empty;
return wip.toOwnedBundle("") catch .empty;
}
/// Parse errors first (AstGen cannot run on a tree that did not parse), then
/// the AstGen pass. Returns how many rows were emitted.
fn treeDiagnostics(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
path: []const u8,
tree: *const Ast,
out: *std.ArrayList(u8),
) !usize {
var n: usize = 0;
if (tree.errors.len != 0) {
const lines: Lines = try .build(arena, tree.source);
for (tree.errors) |e| {
if (n >= max_rows) return n;
var buf: std.ArrayList(u8) = .empty;
var w: std.Io.Writer.Allocating = .fromArrayList(arena, &buf);
tree.renderError(e, &w.writer) catch continue;
const at = tree.tokenStart(e.token);
const lc = lsp.lineCol(tree.source, at);
n += 1;
lsp.row(gpa, out, path, lc.line, lc.col, try std.fmt.allocPrint(arena, "error: {s} {s}", .{
w.written(), std.mem.trim(u8, lines.line(lc.line), " \t"),
}));
}
return n;
}
var bundle = try astCheck(gpa, path, tree);
defer bundle.deinit(gpa);
if (bundle.errorMessageCount() == 0) return n;
for (bundle.getMessages()) |m| {
if (n >= max_rows) return n;
const em = bundle.getErrorMessage(m);
if (em.src_loc == .none) continue;
const sl = bundle.getSourceLocation(em.src_loc);
n += 1;
lsp.row(gpa, out, path, sl.line, sl.column, try std.fmt.allocPrint(arena, "error: {s}", .{
bundle.nullTerminatedString(em.msg),
}));
}
return n;
}
/// `SPC D`: the same check over every `.zig` under the pane's directory. No
/// analyser and no store — AstGen is a per-file pass — so this is bounded by
/// how fast Zig can parse the tree, which for pardes's own `src/` is tens of
/// milliseconds.
fn workspaceDiagnostics(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
io: std.Io,
req: lsp.Req,
out: *std.ArrayList(u8),
) !void {
var files: std.ArrayList([]const u8) = .empty;
try collectZigFiles(arena, io, req.root, &files);
var total: usize = 0;
for (files.items) |path| {
const src = readFileZ(arena, io, path) catch continue;
var tree: Ast = Ast.parse(arena, src, .zig) catch continue;
defer tree.deinit(arena);
total += treeDiagnostics(gpa, arena, path, &tree, out) catch continue;
if (total >= max_rows) return;
}
if (total == 0) lsp.row(gpa, out, req.path, 0, 0, try std.fmt.allocPrint(arena, "no diagnostics in {d} file(s)", .{files.items.len}));
}
// ----------------------------------------------------------- code actions
/// `SPC a`. `code_actions.Builder` is one of the Server-free ones — it wants
/// an arena, an analyser, a handle and an ErrorBundle — so the only missing
/// piece is the bundle, which `astCheck` above produces without a compiler.
///
/// It lists what could be done, and does none of it: the seam hands back rows,
/// not edits, so there is no channel through which a backend could apply a
/// fix. `+Lsp` is prose, so these are lines rather than locations.
fn codeActions(
gpa: std.mem.Allocator,
arena: std.mem.Allocator,
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
out: *std.ArrayList(u8),
) !void {
const tree = &handle.tree;
if (tree.errors.len != 0) {
out.print(gpa, "no code actions: file does not parse\n", .{}) catch {};
return;
}
var bundle = try astCheck(gpa, "", tree);
defer bundle.deinit(gpa);
var builder: zls.code_actions.Builder = .{
.arena = arena,
.analyser = analyser,
.handle = handle,
.offset_encoding = enc,
.only_kinds = null,
};
builder.generateCodeAction(bundle) catch {};
const at = offsets.locToRange(tree.source, .{ .start = off, .end = off }, enc);
builder.generateCodeActionsInRange(at) catch {};
for (builder.actions.items) |a| {
out.print(gpa, "{s}\n", .{a.title}) catch return;
}
if (builder.actions.items.len == 0) out.print(gpa, "no code actions\n", .{}) catch {};
}
// ---------------------------------------------------------------- format
/// `=`. The seam returns ROWS, not edits — there is no channel through which a
/// backend can rewrite the buffer — so format reports rather than formats: one
/// row per line `zig fmt` would change, in the `+Lsp` buffer. That is the
/// honest shape of "format" behind a read-only seam, and it is still the
/// answer to the question the keypress asks.
fn formatQuery(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.ArrayList(u8)) !void {
var tree: Ast = try .parse(arena, req.source, .zig);
defer tree.deinit(arena);
if (tree.errors.len != 0) {
lsp.row(gpa, out, req.path, 0, 0, "cannot format: file does not parse");
return;
}
var buf: std.ArrayList(u8) = .empty;
var w: std.Io.Writer.Allocating = .fromArrayList(arena, &buf);
tree.render(arena, &w.writer, .{}) catch return;
const formatted = w.written();
if (std.mem.eql(u8, formatted, req.source)) {
lsp.row(gpa, out, req.path, 0, 0, "already formatted");
return;
}
var a = std.mem.splitScalar(u8, req.source, '\n');
var b = std.mem.splitScalar(u8, formatted, '\n');
var ln: usize = 0;
var n: usize = 0;
while (n < max_rows) : (ln += 1) {
const x = a.next();
const y = b.next();
if (x == null and y == null) break;
const xs = x orelse "";
const ys = y orelse "";
if (std.mem.eql(u8, xs, ys)) continue;
n += 1;
lsp.row(gpa, out, req.path, ln, 0, try std.fmt.allocPrint(arena, "- {s} + {s}", .{
std.mem.trim(u8, xs, " \t"), std.mem.trim(u8, ys, " \t"),
}));
}
}
// ------------------------------------------------------------------ files
/// Every `.zig` under `root`, skipping the directories a source walk has no
/// business in. Capped, because this runs on a keypress.
fn collectZigFiles(arena: std.mem.Allocator, io: std.Io, root: []const u8, out: *std.ArrayList([]const u8)) !void {
if (root.len == 0) return;
var dir = std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true }) catch return;
defer dir.close(io);
var w = dir.walkSelectively(arena) catch return;
defer w.deinit();
while (out.items.len < max_files) {
const e = (w.next(io) catch break) orelse break;
switch (e.kind) {
.directory => {
if (e.basename.len != 0 and e.basename[0] == '.') continue;
if (std.mem.eql(u8, e.basename, "zig-out")) continue;
if (std.mem.eql(u8, e.basename, "zig-cache")) continue;
w.enter(io, e) catch {};
},
.file => {
if (!std.mem.endsWith(u8, e.basename, ".zig")) continue;
const p = std.fs.path.join(arena, &.{ root, e.path }) catch continue;
try out.append(arena, p);
},
else => {},
}
}
}
fn readFileZ(arena: std.mem.Allocator, io: std.Io, path: []const u8) ![:0]u8 {
return std.Io.Dir.cwd().readFileAllocOptions(io, path, arena, .limited(4 * 1024 * 1024), .of(u8), 0);
}
|