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
|
//! The builtins: one struct each, and nothing hand-maintained about them.
//!
//! Executing a builtin's NAME (middle-click / Tab) runs it through the one
//! dispatcher (Pardes.runBuiltin), no matter where the name appears. The
//! struct's DECL NAME is the user-visible word — the one in the topbar, the
//! one sitting in a tag, the one Help prints, the one you execute — so
//! `std.meta.stringToEnum` is the lookup and there is no name table to sync.
//!
//! A zig file IS a struct, so THIS FILE'S declarations are the list: `all`
//! walks them at comptime and `Builtin` folds an enum out of that, in source
//! order. Adding a builtin is writing one struct here — there is no list to
//! append to and no switch prong to add, so there is nothing to forget. The
//! hand-written enum and the four-hundred-line-away switch this replaces were
//! two lists that had to agree; now the code IS the data.
//!
//! What makes a declaration a builtin is its SHAPE: a struct declaring
//! `pub fn run(Ctx) void`. That is strict enough that Ctx and the two folds
//! below fall out by construction rather than by a blocklist — a helper can
//! never accidentally become a builtin, and a builtin whose run has the wrong
//! signature silently vanishes instead of half-working, which the leader
//! table catches immediately (see leader_path: a missing key path is a
//! compile error).
//!
//! What is NOT here: the key bindings. `leader_path` is ONE table in
//! config.zig next to `topbar_str` and every other syntactic choice — the
//! whole remapping surface belongs in one file a user can read top to bottom,
//! not scattered a line at a time across thirty-three structs.
const std = @import("std");
const pardes = @import("pardes.zig");
const Pardes = pardes.Pardes;
const Pane = pardes.Pane;
const output_pane = @import("output_pane.zig");
const config = @import("config.zig");
/// What a builtin gets to act on. One bundle rather than five parameters
/// because most builtins want two of them and zig rejects the unused rest.
/// `txt` is the executed text (Restore reads its path back out of it) and
/// `arg` the execute's ARGUMENT — text typed after the name, or the selection
/// a mouse chord kept, which is why Grep and Find run straight away when there
/// is one instead of asking. The leader passes "" and null: a key path names a
/// builtin, never an argument.
pub const Ctx = struct {
p: *Pardes,
/// pane `id`, already resolved — the dispatcher's null check is the one
/// guard every builtin used to share.
pane: *Pane,
id: usize,
txt: []const u8,
arg: ?[]const u8,
};
/// Every builtin, in the order they are written below — which is the enum's
/// numeric order. Nothing reads that order: the topbar picks its own subset by
/// name and Help sorts by key path, and nothing persists a builtin's integer
/// (the dump stores tag WORDS), so reordering this file is free.
///
/// A FUNCTION and not a const, and so is Builtin below, for one reason: both
/// walk this file's own declaration list, and a const doing that is a
/// declaration whose value depends on itself — zig rejects it outright. As
/// functions they are only ever a signature to the walk, never a value, so
/// they are not in their own way.
///
/// The walk only ever sees `pub` decls, so the imports above are invisible to
/// it; what it does see and turn away is `all`/`word`/`Builtin` (not types)
/// and `Ctx` (a type, but with no `run`).
pub fn all() []const type {
// the whole body is comptime: a []const type only exists there, and it is
// what makes the decl walk's `d` a compile-time name rather than a value
comptime {
@setEvalBranchQuota(4000); // one pass per decl, and @hasDecl builds a map each
var list: []const type = &.{};
for (@typeInfo(@This()).@"struct".decls) |d| {
// @TypeOf never evaluates its operand, so this turns away `all`
// and `Builtin` by their SIGNATURES — asking for either one's
// VALUE here would be a declaration that depends on itself
if (@TypeOf(@field(@This(), d.name)) != type) continue;
const T = @field(@This(), d.name);
if (@typeInfo(T) != .@"struct") continue;
if (!@hasDecl(T, "run")) continue;
if (@TypeOf(T.run) != fn (Ctx) void) continue;
list = list ++ &[_]type{T};
}
return list;
}
}
/// A builtin's user-visible word: the LAST dotted segment of `@typeName`,
/// because @typeName spells a file-scope struct fully qualified
/// ("builtins.Kill"). Deriving it beats a `pub const name` field per struct,
/// which would be the same word written twice with nothing keeping the two
/// honest. A name that is not a legal identifier would be spelled `@"..."`.
pub fn word(comptime T: type) []const u8 {
const n = @typeName(T);
const dot = std.mem.lastIndexOfScalar(u8, n, '.') orelse return n;
return n[dot + 1 ..];
}
/// The enum: field name = struct name, value = index into `all()`. Everything
/// downstream (leader_path's EnumArray, leader_rows, the topbar's comptime
/// check, stringToEnum) speaks it exactly as it did when it was hand-written.
pub fn Builtin() type {
const cmds = all();
const Tag = std.math.IntFittingRange(0, cmds.len - 1);
var names: [cmds.len][]const u8 = undefined;
for (cmds, 0..) |T, i| names[i] = word(T);
return @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, cmds.len));
}
// ---- session ----
pub const Kill = struct {
pub fn run(c: Ctx) void {
c.p.quit = true;
c.p.emit(.quit);
}
};
pub const Dump = struct {
pub fn run(c: Ctx) void {
c.p.dumpState() catch {};
}
};
pub const Restore = struct {
pub fn run(c: Ctx) void {
var it = std.mem.tokenizeAny(u8, c.txt, " \t");
_ = it.next(); // the word "Restore"
const path = it.next() orelse (c.p.last_dump orelse return);
if (path.len > c.p.restore_buf.len) return;
@memcpy(c.p.restore_buf[0..path.len], path);
c.p.restore_req = c.p.restore_buf[0..path.len];
}
};
// ---- display toggles ----
pub const Debug = struct {
pub fn run(c: Ctx) void {
c.p.show_debug = !c.p.show_debug;
}
};
pub const Colors = struct {
pub fn run(c: Ctx) void {
c.p.colors_on = !c.p.colors_on;
}
};
pub const NextColor = struct {
pub fn run(c: Ctx) void {
c.p.theme_idx = (c.p.theme_idx + 1) % pardes.themes.len;
}
};
pub const Crt = struct {
pub fn run(c: Ctx) void {
c.p.crt_on = !c.p.crt_on;
}
};
// The image pane's three renderer toggles. They used to be words the image tag
// printed and actOnSelection matched by hand; as builtins they are executable
// anywhere, pressable under SPC and listed by `SPC ?`, which is the whole
// reason the tag no longer carries them. Each acts on the pane it runs in and
// is inert anywhere else, the way Save is on a terminal — flipping the field is
// the whole toggle: drawImage re-matches the glyph grid when it sees
// grid_mode/grid_ascii disagree with the live ones.
/// glyph art over the host's pixels
pub const Petscii = struct {
pub fn run(c: Ctx) void {
if (c.pane.image) |*iv| {
iv.petscii = !iv.petscii;
}
}
};
/// the C64 palette or the terminal's own 16
pub const Palette = struct {
pub fn run(c: Ctx) void {
if (c.pane.image) |*iv| {
iv.pmode = if (iv.pmode == .commodore) .terminal else .commodore;
}
}
};
/// add the printable ASCII bitmaps to the matcher's glyph set
pub const Ascii = struct {
pub fn run(c: Ctx) void {
if (c.pane.image) |*iv| {
iv.ascii = !iv.ascii;
}
}
};
// ---- panes and columns ----
pub const Save = struct {
pub fn run(c: Ctx) void {
// an output buffer has no file behind it — nothing to write
if (c.pane.file) |f| if (output_pane.fileTraits(f.output).saves) c.p.emit(.{ .save_file = .{ .pane = @intCast(c.id) } });
}
};
pub const Newcol = struct {
pub fn run(c: Ctx) void {
const free = c.p.freeSlot() orelse return;
if (c.p.ncol >= pardes.MAX_COLS) return;
const nt = c.p.newShell(free, "") catch return;
nt.greet = true;
c.p.layoutAppendColumn(free);
c.p.active = free;
}
};
pub const Del = struct {
pub fn run(c: Ctx) void {
c.p.absorbVWeight(c.id);
c.p.layoutRemove(c.id);
c.p.deinitPane(c.pane);
c.p.panes[c.id] = null;
if (c.p.active == c.id) c.p.active = c.p.prevFocus(c.id) orelse {
c.p.quit = true;
c.p.emit(.quit);
return;
};
}
};
pub const Delcol = struct {
pub fn run(c: Ctx) void {
const f = c.p.layoutFindTerm(c.id) orelse return;
var ids: [pardes.MAX_PANES]usize = undefined;
const nids = c.p.col_n[f.col];
for (0..nids) |k| ids[k] = c.p.col_terms[f.col][k];
for (ids[0..nids]) |tid| {
if (c.p.panes[tid]) |tt| {
c.p.layoutRemove(tid);
c.p.deinitPane(tt);
c.p.panes[tid] = null;
}
}
if (c.p.panes[c.p.active] == null) c.p.active = c.p.prevFocus(c.p.active) orelse {
c.p.quit = true;
c.p.emit(.quit);
return;
};
}
};
pub const Tutor = struct {
pub fn run(c: Ctx) void {
const free = c.p.freeSlot() orelse return;
const nt = c.p.openTutorView(free) catch return;
c.p.placeDoc(c.id, free, nt); // a doc like any other
}
};
pub const Help = struct {
pub fn run(c: Ctx) void {
output_pane.openHelp(c.p, c.id, "");
}
};
// ---- search ----
// The two builtins that ASK for something — Find walks file NAMES under this
// pane's directory, Grep file CONTENTS under every pane's. With an argument
// there is nothing to ask: it IS the pattern, so the walk runs now (this is
// what a `Grep` executed with a selection chorded to it means). Without one
// they arm the same tag input `/` does, and Enter runs it (submitSearch).
pub const Find = struct {
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (pat.len > 0) return c.p.runSearch(c.id, pat, .find);
c.p.startSearch(c.pane, config.find_marker);
}
};
/// Find's sibling: Find matches file NAMES under this pane's directory, Grep
/// matches file CONTENTS under every pane's directory at once.
pub const Grep = struct {
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep);
c.p.startSearch(c.pane, config.grep_marker);
}
};
// ---- the window group ----
// The DESTINATION is the name — a word, the way a tag holds Del or Save —
// because these names live in the same vocabulary as everything else here:
// `Wh` would be a leader key path leaking into the text you can middle-click.
// Plain English words are safe for exactly these five: focus is the cheapest
// thing to change by accident (nothing is edited, closed or written) and the
// way back is the opposite word.
pub const Left = struct {
pub fn run(c: Ctx) void {
c.p.focusDir(c.id, .left);
}
};
pub const Down = struct {
pub fn run(c: Ctx) void {
c.p.focusDir(c.id, .down);
}
};
pub const Up = struct {
pub fn run(c: Ctx) void {
c.p.focusDir(c.id, .up);
}
};
pub const Right = struct {
pub fn run(c: Ctx) void {
c.p.focusDir(c.id, .right);
}
};
/// The file<->terminal hop. "Latest" is already recorded: focus_hist is the
/// MRU sync() rebuilds every update (most recent last), the same list a
/// closing pane hands focus back through — so this walks it instead of keeping
/// a second one. Which side is which: only a shell is a terminal; a file, an
/// image and an output buffer (+Search/+Help) are all DOCS you read, so
/// isTerminal is the whole test. Landing pushes this pane to the top of that
/// same history, which is why the hop back is the same key.
pub const Toggleterm = struct {
pub fn run(c: Ctx) void {
const want_term = !c.pane.isTerminal();
var t: ?usize = null;
// any live pane of the other kind: a pane you have never focused (the
// file you started with) is in no history at all
for (c.p.panes, 0..) |slot, k| {
const op = slot orelse continue;
if (k != c.id and op.isTerminal() == want_term) t = k;
}
// ...but the most recently focused one wins
var i = c.p.nfocus;
while (i > 0) {
i -= 1;
const hid = c.p.focus_hist[i];
const hp = c.p.panes[hid] orelse continue;
if (hid != c.id and hp.isTerminal() == want_term) {
t = hid;
break;
}
}
if (t) |target| {
c.p.active = target;
c.p.panes[target].?.pending = 0;
}
}
};
// ---- the language group ----
// Reached as `SPC l <helix's letter>` — see leader_path for why the prefix
// exists. They are builtins rather than bare keys for the same reason Save is
// one: the word is executable wherever it appears, so a middle-click on
// `Hover` in a tag does what `SPC l k` does. The five GOTOS are not here —
// helix binds them under `g` as motions, and a motion has no business being a
// word you can click.
//
// Most of them are one call: ask, and let the answer land in lspResponse.
// Nothing here blocks or knows what a backend is — swapping backends changes
// lsp.query and not one line below.
pub const Hover = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .hover, "");
}
};
pub const CodeAction = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .code_action, "");
}
};
pub const SelectRefs = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .select_refs, "");
}
};
pub const Symbols = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .document_symbols, "");
}
};
pub const Diagnostics = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .diagnostics, "");
}
};
pub const WsDiagnostics = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .workspace_diagnostics, "");
}
};
// The two that need a word from the user, handled exactly the way Find and
// Grep are: an argument means run it now (a selection chorded onto the name),
// no argument arms the tag input and Enter submits (submitSearch).
pub const Rename = struct {
pub fn run(c: Ctx) void {
const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (a.len > 0) return c.p.lspRequest(c.id, .rename, a);
c.p.startSearch(c.pane, config.rename_marker);
}
};
pub const WsSymbols = struct {
pub fn run(c: Ctx) void {
const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (a.len > 0) return c.p.lspRequest(c.id, .workspace_symbols, a);
c.p.startSearch(c.pane, config.symbol_marker);
}
};
// Introspection. A language backend that answers nothing looks exactly like
// one that is broken — from the outside, `gd` doing nothing is both "there is
// no definition" and "the analyser threw and we swallowed it". These two are
// how you tell: Lspinfo says what the backend IS, Lspwhy says what it just DID
// and where it stopped.
pub const Lspinfo = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .status, "");
}
};
pub const Lspwhy = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .explain, "");
}
};
|