//! 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"); /// The installed fonts, for the two builtins at the bottom of this file. A /// GUI-only file behind a comptime branch, the way look.zig imports the web's /// source archive: the tty and web builds evaluate the other arm and compile /// none of it. const fonts = if (pardes.platform == .gui) @import("gui/fonts.zig") else struct {}; /// 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)); } // ---- the two acme verbs ---- // Look and Execute are the verbs the whole environment is built on, and they // are BUILTINS: `Look main.zig` typed in a tag and executed is the same look a // right click on `main.zig` is, `Exec ls` the same as a middle click on `ls`. // The mouse buttons and Enter/Tab are not a second path into them any more — // they are two bindings pointing here (config.look_cmd / exec_cmd), the status // `SPC f s` has relative to Save. That is the whole feature: what used to be a // `button` parameter threaded through every keyboard call site, with the // builtin dispatch nested INSIDE it, is now one word each. // // The operand is `arg` in both — a name's tail (`Look main.zig`), else the // selection a chord kept, else the word the gesture pointed at, which the // gesture resolves and passes. Nothing to act on means nothing happens, the // way `Save` on a terminal is inert. pub const Look = struct { pub fn run(c: Ctx) void { c.p.lookAt(c.id, c.arg orelse return); } }; pub const Exec = struct { pub fn run(c: Ctx) void { // the destination pane is Look's business (it focuses what answered); // an execute deliberately leaves you where you were _ = c.p.execute(c.id, c.arg orelse return); } }; // ---- 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; } }; /// One step along the ring. With 228 themes in it this is no longer a way to /// REACH a theme — ThemeSel is — but it is still the way to browse one, and the /// browse got better rather than worse: the generated half is sorted by name, so /// the neighbours of wherever you are are that theme's own variants (light, /// hard, soft, the whole gruvbox family in a row). Kept as the topbar word and /// SPC t n it has always been; a ring you can walk off the end of in three /// clicks was never what made it useful. pub const NextColor = struct { pub fn run(c: Ctx) void { c.p.theme_idx = (c.p.theme_idx + 1) % pardes.themes.len; } }; /// The theme BY NAME — `Theme acme`. The ring grew past the point where /// cycling to the one you want is reasonable, so this is the way to ask for /// one, and NextColor stays as the way to browse. Inert without an argument /// (there is no theme called nothing), which is also why it has no leader path: /// a key path names a builtin and can never carry the name of a theme. /// /// A LINEAR SCAN over 228 names, on a keystroke: the alternative is a comptime /// name->index map, which is a second copy of the ring to build for a lookup /// nobody will ever measure. 228 short string compares is microseconds, and it /// happens once per theme change, not once per frame. pub const Theme = struct { pub fn run(c: Ctx) void { const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); for (pardes.themes, 0..) |t, i| { if (std.mem.eql(u8, t.name, want)) { c.p.theme_idx = i; return; } } } }; /// ...and the list of what Theme takes, as a buffer you walk. Its rows are /// `Theme ` COMMANDS rather than locations, so n/N execute them instead /// of looking them (output_pane.Traits.executes) and stepping the list wears /// each theme in turn — the picker is the list, and there is nothing to /// confirm because arriving already applied it. pub const ThemeSel = struct { pub fn run(c: Ctx) void { output_pane.openThemes(c.p, c.id); } }; pub const Crt = struct { pub fn run(c: Ctx) void { c.p.crt_on = !c.p.crt_on; } }; // ---- the GUI's font, and NOTHING on any other platform ---- // // Theme and ThemeSel again, one layer down: a word that takes a name, and the // list of what it takes. What is different is that these two only EXIST in a // gui build, and the mechanism is the one this file's header describes rather // than a new one — `all()` folds the enum out of the SHAPE of each decl, so a // struct whose `run` is not `fn (Ctx) void` is not a builtin. Here `run` is a // void const on tty and web: the enum has no field, `SPC ?` has no row, the // dispatcher has no prong, and nothing in a tty binary ever opens a font // directory. The body sits inside the struct as an ordinary private decl, // which the walk never sees (it only reads pub, file-scope declarations) and // which nothing on those platforms ever analyses. // // A `pub const Font = if (gui) struct {...} else struct {}` would read better // and is WRONG: zig names a struct born inside an if-expression // "builtins.Font__struct_32751", and that name is the user-visible word. /// The GUI font BY NAME — `Font DejaVuSansMono-Regular`, the way `Theme ` /// takes a theme, and inert without an argument for the same reason (there is /// no font called nothing). The name is a font FILE's stem, which is what the /// picker lists; resolving it is a walk of the font directories that stops at /// the first match, so nothing is cached and an install five seconds ago is /// findable. /// /// The core cannot load a font — it has no rasterizer, no atlas and no window /// — so this asks: the resolved PATH goes in fonts.want, the shell takes it on /// its next pass and re-rasters. Exactly the shape Restore already has. pub const Font = struct { pub const run = if (pardes.platform == .gui) apply else {}; fn apply(c: Ctx) void { const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); const hit = fonts.list(c.p.scratch.allocator(), want); if (hit.len == 0) return; const path = hit[0].path; if (path.len > fonts.want_buf.len) return; @memcpy(fonts.want_buf[0..path.len], path); fonts.want = fonts.want_buf[0..path.len]; } }; /// ...and the list of what Font takes: every MONOSPACE font on the machine, /// one `Font ` row each, in the picker ThemeSel already is (rows that /// are commands, so n/N execute them — walking the list wears each font in /// turn and picking one is stopping on it). /// /// Monospace only, which is the one judgement in the feature: the grid is a /// fixed cell, so a proportional face is not a worse-looking option but an /// unreadable one — and this picker EXECUTES what it steps onto, so listing /// them would mean the list wearing one on the way past. See fonts.monospaced. pub const FontSel = struct { pub const run = if (pardes.platform == .gui) apply else {}; fn apply(c: Ctx) void { output_pane.openFonts(c.p, c.id); } }; /// Toggle a native PDF between the reading-oriented fit-width view and the /// whole-page-height view. Like Font on non-GUI builds, this declaration's /// `run` deliberately has the wrong shape when MuPDF is disabled: `all()` then /// omits it entirely, so the enum, Help and runtime binary contain no /// PdfFit. pub const PdfFit = struct { pub const run = if (pardes.pdf_enabled) apply else {}; fn apply(c: Ctx) void { c.p.togglePdfFit(c.pane); } }; // The image pane's three renderer toggles. They used to be words the image tag // printed and the execute dispatcher matched by hand; as builtins they are // executable anywhere, pressable under SPC and listed by `SPC ?`, 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, .top); 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, .top); 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: the jump stack runs /// oldest-first and a closing pane hands focus back through the same list — 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 onto 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.njumps; while (i > 0) { i -= 1; const hid = c.p.jumps[i].pane; 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 jump group ---- // Where focus HAS BEEN, as three verbs and a list over the one stack pardes // keeps (Pardes.jumps — see trackJump for what gets onto it). Builtins rather // than bare key handlers for the same reason the four directions are: one // implementation, reachable by chord, by `SPC j ...`, and by executing the // word wherever it is written. /// Ctrl-o: one step back into the history. pub const Back = struct { pub fn run(c: Ctx) void { c.p.jumpBy(-1); } }; /// Ctrl-i: one step forward again, up to wherever Back started. pub const Forward = struct { pub fn run(c: Ctx) void { c.p.jumpBy(1); } }; /// vim's Ctrl-^: the pane you were in before this one, whichever it was — the /// hop you press twice a minute and never want to count steps for. /// /// It does NOT move the stack cursor: it goes somewhere, so trackJump records /// it like any other move, and that is exactly what makes it an involution — /// after the hop, the pane you came from is the newest OTHER pane, so pressing /// it again comes straight back. Back/Forward walk history; this one makes it. pub const Last = struct { pub fn run(c: Ctx) void { var i = c.p.njumps; while (i > 0) { i -= 1; const j = c.p.jumps[i]; if (j.pane != c.id) return c.p.focusPaneLine(j.pane, .{ .line = j.line, .col = j.col }); } } }; /// The same stack, as text you can read and click. Not a copy of it and not a /// second list kept in step — the buffer is RENDERED from the stack when you /// ask, the way +Search is rendered from a walk. pub const Jumplist = struct { pub fn run(c: Ctx) void { output_pane.openJumps(c.p, c.id); } }; // ---- the language group ---- // Reached as `SPC l ` — 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, ""); } };