//! Every syntactic choice pardes makes, in one file: which key runs what, //! which mouse button means what, how a looked-at word is SPELLED, and the //! handful of layout numbers that are taste rather than structure. //! //! The point is the editing session, not the architecture: retargeting a key, //! a chord or a piece of Look syntax is an edit HERE and nowhere else. Nothing //! below is read at runtime from a file — this IS the config format, recompiled //! — so a binding may be any comptime expression and a wrong one is a compile //! error rather than a silent no-op. //! //! Order is deliberate. PART 1 is pardes's OWN vocabulary, the part a user //! actually fiddles with, so it is where a reader lands. PART 2 is Look/Exec //! syntax. PART 3 is the helix keymap, which is under a differential-testing //! contract — see the banner there before touching it. //! //! What is NOT a binding: a named key's own identity. Insert mode's Backspace, //! Delete, Enter and Tab are dispatched on `Key.` in handleInsert and //! stay there — Backspace deleting backwards is what the key IS, not a choice //! anyone remaps. `Ctrl-w` deleting a word backwards is a choice, and it is //! here. const std = @import("std"); const pardes = @import("pardes.zig"); const builtins = @import("builtins.zig"); const Key = pardes.Key; const Mouse = pardes.Mouse; const Builtin = builtins.Builtin(); /// One key press a binding matches. `shift` is only consulted when a binding /// ASKS for it: shift is already carried in the codepoint for anything /// printable (`A` is `A`, not shift-`a`), so the one chord that needs the flag /// is Shift-Esc, on a key that has no shifted codepoint. pardes.zig's `hit` /// is the matcher. pub const Chord = struct { cp: u21, ctrl: bool = false, alt: bool = false, shift: bool = false, }; // A binding is a LIST because most have two spellings that must reach the same // arm — a letter and an arrow, `Ctrl-f` and PageDown. One list, one dispatch // site; the `or` chains this replaces had the modifier logic written out three // ways (the old is/isC/isA). // ============================================================================ // PART 1 — pardes's own bindings. Nothing here is inherited from anywhere; // these are the ones to fiddle with. // ============================================================================ // ---- the SPC leader ---- /// opens the leader: a key path from here runs a BUILTIN with no arguments. /// Body normal mode only — a tag is always insert, and a tty pane's keys /// belong to the program. pub const leader: []const Chord = &.{.{ .cp = ' ' }}; /// Help's key, honored at ANY depth: it lists what the prefix typed so far can /// still reach. Also Help's own path below, so the character is spelled once. pub const leader_help: u8 = '?'; /// what an unlisted builtin's path is until someone says otherwise: a value no /// key path can be, so the loop at the bottom of the table can refuse it const undecided: []const u8 = ""; /// SPC leader: ONE key path per builtin, the whole remapping surface. A new /// enum field is a compile error until someone has DECIDED its path — that is /// what `undecided` and the loop under the table are for, and it used to be /// EnumArray.init's own doing (it demands every field). It cannot be any more: /// the gui-only builtins are not fields of this literal's type on tty or web, /// so the literal cannot name them and a default is the only way to have both. /// Groups are just shared first letters (f files, h docs, c columns, t /// toggles, s session, l language, w windows). /// /// `null` = this builtin's shortcut is not a leader path. Look and Exec are /// the two: their shortcuts are Enter/Tab and the two mouse buttons below, and /// a third spelling under SPC would be a key that does nothing you cannot /// already do with the key your hand is on. The option is the honest type — /// "every builtin has a leader path" was only ever true by accident. pub const leader_path = paths: { var table = std.EnumArray(Builtin, ?[]const u8).initDefault(@as(?[]const u8, undecided), .{ .Help = &[_]u8{leader_help}, // The whole LANGUAGE group lives under `l`, and pardes's own builtins keep // the letters they always had — `SPC d` is Del, `SPC k` is Kill. // // Helix puts these on bare `` letters, and an earlier pass followed // it there, which cost `d`, `k`, `s`, `h` and the session group. That is // the wrong trade: those five are pardes's most-pressed keys and predate // the language work, whereas an LSP command is something you reach for // deliberately and can afford one more keystroke. Each one still keeps // HELIX'S OWN LETTER inside the group, so the mapping is `X` -> // `SPC l X` with nothing to re-learn but the prefix. // // The five GOTOS are untouched and remain exactly helix's — `gd` `gD` `gy` // `gi` `gr`, plus `]d`/`[d` and `=`. Those never collided with anything, so // there was never a reason to move them. They are in PART 3. .Hover = "lk", .Rename = "lr", .CodeAction = "la", .SelectRefs = "lh", .Symbols = "ls", .WsSymbols = "lS", .Diagnostics = "ld", .WsDiagnostics = "lD", .Lspinfo = "li", .Lspwhy = "lw", .Del = "d", .Kill = "k", // the `f` file group (spacemacs): Save left vim's `w` to join Find here, // which frees `w` for the window group (SPC w h/j/k/l) to move into. .Save = "fs", .Find = "ff", .Grep = "fg", .Tutor = "ht", .Newcol = "cn", .Delcol = "cd", .Debug = "td", .Colors = "tc", .NextColor = "tn", .Crt = "tr", // the theme picker joins the toggles it belongs with; `Theme` itself takes // a NAME, and a key path can never carry one, so it has none (the same // reason Look and Exec have none) .ThemeSel = "tt", .Theme = null, // the image toggles join the same `t` group; Palette takes `l` because // `p` is Petscii's and `c` is Colors'. .Petscii = "tp", .Palette = "tl", .Ascii = "ta", .Dump = "sd", .Restore = "sr", // the `w` window group `Save` vacated: the four directional focus moves // the Ctrl-w prefix does, spelled h/j/k/l because focus IS a motion, plus // `t` for the file<->terminal hop. .Left = "wh", .Down = "wj", .Up = "wk", .Right = "wl", .Toggleterm = "wt", // the `j` JUMP group, its own letter rather than more of `w`: the window // group moves focus by GEOMETRY (the pane left of this one), these move it // by TIME (the pane I was in before). `o` and `i` are the letters of the // chords that do the same thing, `jj` is the group's obvious verb, and // `jl` is the list itself. .Back = "jo", .Forward = "ji", .Last = "jj", .Jumplist = "jl", // the two acme verbs: keys and buttons, no leader path — see above .Look = null, .Exec = null, }); // The GUI's two font builtins, in the same `t` group as the theme picker // they mirror — but set here rather than named above, because on tty and // web they are not builtins at all (see builtins.zig) and the literal's // type has no field to write. `Font` takes a NAME, so it has no path, for // the same reason `Theme` has none. if (pardes.platform == .gui) { table.set(.FontSel, "tf"); table.set(.Font, null); } // PdfFit exists only in MuPDF builds (its run declaration deliberately // changes shape when the feature is off), so name its toggle path through // the same comptime branch rather than making feature-off enums mention it. if (pardes.pdf_enabled) table.set(.PdfFit, "tz"); // ...and the property EnumArray.init used to give for free: every builtin // this build HAS is a builtin someone decided a path (or a null) for. for (std.enums.values(Builtin)) |b| if (table.get(b)) |p| { if (std.mem.eql(u8, p, undecided)) @compileError("builtin has no leader path decided: " ++ @tagName(b)); }; break :paths table; }; // ---- the acme chords ---- // Look and Execute, the two verbs the whole environment is built on. Each has // a KEY and a mouse BUTTON, and they are the same two verbs: Enter on a word // does what a right click on it does. Swap the two `_key` lines for the vim // reading, where Enter runs the command line. (This pair replaced a // `swap_enter_tab` bool, which could only ever exchange them — two bindings // can also be moved somewhere else entirely.) pub const look_key: []const Chord = &.{.{ .cp = Key.enter }}; pub const exec_key: []const Chord = &.{.{ .cp = Key.tab }}; pub const look_button: Mouse.Button = .right; pub const exec_button: Mouse.Button = .middle; /// sweep, focus, place the cursor, and the left half of every acme chord pub const select_button: Mouse.Button = .left; /// ...and WHAT those four run. Look and Exec are ORDINARY builtins — the same /// kind of thing Save and Grep are, executable by name wherever text lives /// (`Look main.zig` in a tag does what a right click on `main.zig` does) — so /// the four bindings above are bindings like any other, and these two lines /// are the whole of what makes them special. Point `look_cmd` at `.Grep` and /// Enter greps. pub const look_cmd: Builtin = .Look; pub const exec_cmd: Builtin = .Exec; // ---- windows ---- /// helix's window prefix. It stays despite `SPC w` covering the same four /// builtins because it reaches one place the leader cannot: a pane in raw tty /// mode never sees SPC (the shell owns every printable key), so this is the /// only keyboard way out of one. pub const window_prefix: []const Chord = &.{.{ .cp = 'w', .ctrl = true }}; /// The four directional focus moves, as data: `Ctrl-w ` in a /// body, and the bare LETTER on a focused tagline, where focus IS the motion /// (the arrows stay grapheme motion inside the tag, which is why the two /// spellings are separate fields rather than one list). One table, two /// readers — and the `cmd` column is what makes the chord discoverable from /// the builtin as well as the other way round. pub const window_keys = [_]struct { letter: Chord, arrow: Chord, cmd: Builtin }{ .{ .letter = .{ .cp = 'h' }, .arrow = .{ .cp = Key.left }, .cmd = .Left }, .{ .letter = .{ .cp = 'j' }, .arrow = .{ .cp = Key.down }, .cmd = .Down }, .{ .letter = .{ .cp = 'k' }, .arrow = .{ .cp = Key.up }, .cmd = .Up }, .{ .letter = .{ .cp = 'l' }, .arrow = .{ .cp = Key.right }, .cmd = .Right }, }; /// global window ops, live in ANY mode (which is why they are Alt-, not a /// leader path): a new terminal below, and moving this terminal into a fresh /// column. Alt-c is a deliberate divergence from helix's change-noyank — /// hxdiff waives it by name. pub const new_shell_below: []const Chord = &.{.{ .cp = 'n', .alt = true }}; pub const pane_to_new_column: []const Chord = &.{.{ .cp = 'c', .alt = true }}; // ---- jumps ---- /// vim's Ctrl-o / Ctrl-i, walking the focus history back and forward. Global /// in any mode, like the two Alt- ops above and for the same reason: getting /// BACK has to work from inside a pane that owns its keys. /// /// CAREFUL, and this is why the table has a comment: **Ctrl-i is Tab**. On the /// wire they are the same byte (0x09), so on a host that speaks only the /// legacy encoding this binding never fires and 0x09 keeps meaning `exec_key` /// below — which is the right way round, since Tab-executes is the older and /// more used of the two. Where the host speaks the kitty keyboard protocol /// (`CSI 105;5u`) the two are distinct keys and both work. Shift-Esc /// (tty_toggle_alt) is already spelled on that same bet. /// /// Shaped like window_keys: the `cmd` column is what makes the chord /// discoverable from the builtin as well as the other way round, and it is /// what keeps ONE implementation — pressing the chord and executing the word /// `Back` are the same call. pub const jump_keys = [_]struct { chord: Chord, cmd: Builtin }{ .{ .chord = .{ .cp = 'o', .ctrl = true }, .cmd = .Back }, .{ .chord = .{ .cp = 'i', .ctrl = true }, .cmd = .Forward }, }; /// `j` off the topbar drops back onto a tagline — the mirror of the `k` that /// got you there (window_keys' letter, answered by tagNormalKey). pub const topbar_down: []const Chord = &.{.{ .cp = 'j' }}; /// the topbar has no neighbouring window to walk to, so up here h/l are plain /// grapheme motion instead pub const topbar_left: []const Chord = &.{.{ .cp = 'h' }}; pub const topbar_right: []const Chord = &.{.{ .cp = 'l' }}; // ---- raw tty mode ---- /// Ctrl- toggles raw tty mode in and out (terminals only); tty is /// deliberately off the normal editing path. Not a Chord because the shell can /// override it at runtime (`--tty-toggle`), so this is only Options' default. pub const tty_toggle_default: u21 = 'b'; /// the second spelling, for hosts that report modifiers on Escape (the kitty /// keyboard protocol). Where they don't it arrives as a plain Escape and still /// means what Escape always means. pub const tty_toggle_alt: []const Chord = &.{.{ .cp = Key.escape, .shift = true }}; // ---- the tag line and the topbar ---- // The topbar is a HAND-PICKED subset in a fixed order, not a derivation: row 0 // is where topbar clicks land, so its exact bytes are load-bearing (every // snapshot golden records the column each word starts at). Comptime-checked // against the enum in pardes.zig so a rename cannot silently rot it. // Colors and Crt are NOT here: both are set-once display switches you flip and // forget, and a bar you read every frame should not spend width on them now // that `SPC t c` / `SPC t r` press them. NextColor stays — it is the one you // cycle repeatedly, so a click beats a three-key path. // // Help is LAST and is the one word that has to be here. A bare `pardes` boots // straight into tty mode (main.zig: `args.len == 1`), where every printable // key belongs to the shell — so SPC never reaches the leader and `SPC ?`, the // thing that would tell you the leader exists, is exactly what you cannot // press. Row 0 is not a pane, so a middle-click on it is dispatched before any // pane's mode is consulted: this word works in tty mode, which is the only // reason it earns the width. Appended rather than inserted so every existing // word keeps its column and no golden's click coordinates move. // // The two searches go LAST (before the optional `Restore `), and next to // each other: they are one pair — Find matches file NAMES, Grep their CONTENTS. pub const topbar_str = "Kill Newcol Tutor Debug NextColor Dump Find Grep Help"; /// the default editable tail of a pane's tag, per kind (an output buffer has /// no file to Save, so it gets the plain one) pub const pane_builtins_str = "Del"; pub const file_pane_builtins_str = "Save Del"; /// the pane's mode, as ONE character in the layout box at its top-left — live /// chrome, not text you own. It used to be a three-letter word leading every /// tagline; the box was already there carrying no information at all, so the /// mode moved into it and the taglines got their four columns back. /// /// These are NOT the initials. A badge you read at a glance every time your /// eye crosses a pane should LOOK like what it means, and each of these is a /// mark that already means its mode somewhere else: `^` is the proofreader's /// caret, the mark that says text goes in HERE; `$` is the shell prompt, and a /// pane wearing it has the keyboard wired straight to the program on the other /// end; `•` is the pane at rest, a full stop, nothing waiting to eat what you /// type. (The caret's true form is `‸` U+2038 and the ASCII `^` is only its /// stand-in — but `^` is in every font ever made and `‸` is in about four, and /// a mode badge that renders blank on someone's terminal is worse than one /// spelled with the near-miss.) /// /// ONE CODEPOINT each. The box prints a single cell, so a two-character string /// here would be pushed into one cell as a single grapheme and come out wrong. /// A font missing the glyph draws a blank box, which is exactly what the box /// drew before there was anything in it. pub const tag_normal = " "; pub const tag_insert = "^"; pub const tag_tty = "$"; /// ...and `img` stays a WORD at the head of an image pane's tagline, because /// it is not a mode: it says what the pane IS, which no amount of watching the /// box will tell you. The box on an image pane still shows its mode. pub const tag_image = "img"; /// on a focused tag: yank what the chord would run (the selection, else the /// word under the cursor). The path is selectable, so this is how you copy it. pub const tag_yank: []const Chord = &.{.{ .cp = 'y' }}; // ---- the command line and search ---- /// vim's command line with acme's vocabulary: focus the pane's own tag in /// normal mode, parked at the tail's start, and the execute chord runs the /// word under the cursor (`:w` = Save). pub const command_line: []const Chord = &.{.{ .cp = ':' }}; /// `/` types a pattern into the tag; n/N walk the results. Same keys on every /// kind of pane — a terminal with no search armed falls back to n/N as a /// motion over the lookable tokens in its output. /// /// Helix's letters, but NOT helix's commands (it searches by regex and pardes /// has no regex engine), so these three sit out here rather than under the /// contract in PART 3 — the differential suites never press them. pub const search: []const Chord = &.{.{ .cp = '/' }}; pub const search_next: []const Chord = &.{.{ .cp = 'n' }}; pub const search_prev: []const Chord = &.{.{ .cp = 'N' }}; /// Enter on an armed search input runs it; `escape` (PART 3) abandons it. pub const search_submit: []const Chord = &.{.{ .cp = Key.enter }}; // ---- mouse ---- /// wheel step, in rows / in columns. The horizontal step is bigger because a /// column is narrower than a row is tall and a wheel tick should move a /// comparable distance either way. pub const wheel_rows: i32 = 1; pub const wheel_cols: i32 = 4; /// Touchpad drift guard, in ticks. A two-finger swipe that is MEANT to be /// vertical carries a little sideways drift, and the pad faithfully turns that /// drift into wheel_left/wheel_right — so a plain scroll slides the view /// sideways under you. Every vertical tick re-arms the guard to this many /// ticks and every horizontal tick spends one instead of scrolling, which /// makes horizontal EARN its way back: it has to land this many ticks in a row /// with no vertical among them. 3, because drift arrives in ones and twos — /// at 1 or 2 a doubled drift tick mid-swipe still gets through, and much /// higher starts eating deliberate swipes. Set 0 to disable the heuristic. /// /// It costs nothing at rest: the guard is only armed by vertical scrolling, so /// a horizontal swipe that starts from a still view moves on its FIRST tick. /// Note this applies to a tilt wheel too, where "recent vertical" is a much /// weaker signal of accident — a mouse would rather not have it. Living with /// that is deliberate: the only honest fix is a per-device flag out of the /// shell (libinput/SDL know which is which, vaxis does not), and paying for a /// device-detection layer to spare a tilt wheel three clicks after a scroll is /// a worse trade than the three clicks. pub const wheel_guard_ticks: u8 = 3; /// The whole guard, as one state machine, so it can be tested as one thing: /// fold a wheel tick into `guard` and answer whether it scrolls. The core owns /// the counter (Pardes.wheel_guard) because the gesture belongs to the DEVICE, /// not to whichever pane the pointer happens to sit over. pub fn wheelTick(guard: *u8, vertical: bool) bool { if (vertical) { guard.* = wheel_guard_ticks; return true; } if (guard.* == 0) return true; guard.* -= 1; return false; } // written to hold for ANY tuning of wheel_guard_ticks, since tuning it by hand // is what this file is for — a test that pinned the number 3 would just be a // second place to edit it test "wheel drift guard" { var g: u8 = 0; // from rest, horizontal moves on the first tick — nothing to prove try std.testing.expect(wheelTick(&g, false)); try std.testing.expect(wheelTick(&g, false)); if (wheel_guard_ticks == 0) return; // guard disabled: nothing left to check // a vertical swipe with drift mixed in: every sideways tick is swallowed, // because each vertical tick re-arms the guard in full for (0..4) |_| try std.testing.expect(wheelTick(&g, true)); try std.testing.expect(!wheelTick(&g, false)); try std.testing.expect(wheelTick(&g, true)); try std.testing.expect(!wheelTick(&g, false)); // the deliberate horizontal swipe that follows pays off the rest of the // guard tick by tick, then runs free for (1..wheel_guard_ticks) |_| try std.testing.expect(!wheelTick(&g, false)); try std.testing.expect(wheelTick(&g, false)); try std.testing.expect(wheelTick(&g, false)); } // ---- layout numbers that are taste ---- /// the file pane's line-number gutter, in columns pub const PREFIX_W: u16 = 5; /// vim 'scrolloff': keyboard cursor moves keep this many context rows visible /// above/below the cursor (clamped at file boundaries and short panes), and /// the same count of COLUMNS horizontally pub const scroll_off = 3; /// the pane's left chrome: scrollbar + the layout box in the tag row. The /// scrollbar PAINTS only the first of these columns; the second is the pane's /// own background, so the bar reads as one column with a column of page /// between it and the text. Layout is untouched by that — the gutter is still /// GUTTER columns and a click anywhere in them still scrolls; only the ink /// narrowed. A half-block glyph in the second column was tried and rejected. pub const GUTTER: u16 = 2; /// a pane never shrinks past this (the h-handle can still take it to its tag /// row alone, which is BOX_H and a structural fact, not this) pub const MINW: u16 = 10; pub const MINH: u16 = 3; // ============================================================================ // PART 2 — Look/Exec syntax: how a click on text is SPELLED. What the // resolution then DOES with it is look.zig. // ============================================================================ /// file-ish word chars (acme's isfilec): alnum + these. This set is the whole /// definition of "the word under the cursor" for Look, Execute, the tag chord /// and every search result row. pub fn isFileChar(c: u8) bool { return std.ascii.isAlphanumeric(c) or switch (c) { '.', '-', '+', '/', ':', '@', '_', '~' => true, else => false, }; } /// `` @`ls -la` `` — a word that names a COMMAND to run rather than a file to /// open. Both halves are named here because they are a CHOICE: the `@` marks /// it as ours (it is already a file char, so it can never split a path) and /// the backquotes hold a command line with spaces in it, which is the whole /// point — a file-ish word cannot. Respell them here and nowhere else. pub const cmd_open = "@`"; pub const cmd_close: u8 = '`'; /// The command inside `` @`...` ``, or null when `w` is not one. pub fn commandWord(w: []const u8) ?[]const u8 { if (w.len <= cmd_open.len or !std.mem.startsWith(u8, w, cmd_open)) return null; if (w[w.len - 1] != cmd_close) return null; return w[cmd_open.len .. w.len - 1]; } /// The bounds of the word at `col` in `line` — THE expansion a no-drag /// look/execute click and the tag chord both use. /// /// A `` @`...` `` run is taken WHOLE and wins outright: a backtick is not a /// file char, so the plain scan below would stop dead inside one and hand a /// look the fragment `ls` out of `` @`ls -la` ``. Acme does exactly this for /// its own `<`/`|`/`>` command words. Otherwise it is the file-ish word. pub fn wordBounds(line: []const u8, col: usize) struct { lo: usize, hi: usize } { var i: usize = 0; while (std.mem.indexOfPos(u8, line, i, cmd_open)) |o| { const close = std.mem.indexOfScalarPos(u8, line, o + cmd_open.len, cmd_close) orelse break; if (col >= o and col <= close) return .{ .lo = o, .hi = close + 1 }; i = close + 1; } var lo = col; while (lo > 0 and isFileChar(line[lo - 1])) lo -= 1; var hi = col; while (hi < line.len and isFileChar(line[hi])) hi += 1; return .{ .lo = lo, .hi = hi }; } /// separates a path from its LINE and COL: `main.zig:100:7`. Must be a member /// of isFileChar or the suffix would not be part of the word in the first /// place. pub const line_col_sep: u8 = ':'; /// ...and separates that spot from the END of a RANGE. A look at a ranged path /// SELECTS the span rather than just parking on its first cell, which is what /// lets a search result carry the text it matched and `n` land ON it. /// /// Three spellings. The long one subsumes the other two, but the short ones /// are what a person actually types and what a grep-alike emits, so all three /// parse: /// main.zig:412-418 lines 412 through 418, whole /// main.zig:412:9-21 line 412, columns 9 through 21 /// main.zig:412:9-418:1 line 412 column 9 through line 418 column 1 /// Both ends are INCLUSIVE and 1-based, like the spot they extend — `412-418` /// reads as seven lines, not six. `main.zig:412` and `main.zig:412:9` keep /// meaning exactly what they always did. /// /// Must be an isFileChar member, same as the separator above, or a click would /// expand to half a range. That is also why reading it is FUSSY (look.zig, /// parsePathLine): ordinary paths are full of dashes, so the suffix counts as /// a range only when a NUMBER follows the dash — `my-file:10` and `build-2` /// stay the paths they are. pub const range_sep: u8 = '-'; /// `@p7:10:5` — pane 7, line 10, column 5. The one look target that names a /// live pane instead of a path, because terminals and output buffers have no /// file for a location to point at. Both the writer (a `/` result row) and the /// reader (look.resolve) spell it from here. pub const pane_addr = "@p"; /// a word starting with one of these is a URL and leaves the app entirely: no /// filesystem can answer it pub const url_schemes = [_][]const u8{ "http://", "https://" }; /// a path ending in one of these opens an image pane instead of a file pane pub const image_exts = [_][]const u8{ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ppm", ".pgm", ".tga" }; /// What `Ctrl-c` (comment_toggle) puts at the front of a line, by file /// EXTENSION — which is how src/syntax.zig already tells one language from /// another, so this is that same notion and not a second one. A pane whose /// path matches nothing here (and every terminal, which has no path at all) /// gets `comment_token_default`. /// /// `#` as the default is not a guess: it is helix's own DEFAULT_COMMENT_TOKEN, /// which is what a helix buffer with no language configured comments with — /// and therefore what the differential oracle answers, since the harness runs /// with zero language configs. /// Languages with no LINE comment at all (css, html, json, ocaml) are absent /// on purpose: helix leaves those buffers on its default too, and inventing a /// token for them would be a divergence nothing asked for. pub const comment_token_default = "#"; pub const comment_tokens: []const struct { exts: []const []const u8, token: []const u8 } = &.{ .{ .token = "//", .exts = &.{ ".zig", ".zon", ".c", ".h", ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".rs", ".go", ".java", ".scala", ".sc", ".kt", ".kts", ".cs", ".csx", ".php", ".pas", ".pp", ".p", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".swift", ".dart" } }, .{ .token = "#", .exts = &.{ ".py", ".pyw", ".sh", ".bash", ".zsh", ".rb", ".rake", ".ex", ".exs", ".ps1", ".psm1", ".psd1", ".pl", ".pm", ".r", ".jl", ".nix", ".toml", ".yaml", ".yml", ".cmake", ".mk", ".tf" } }, .{ .token = "--", .exts = &.{ ".lua", ".hs", ".lhs", ".elm", ".sql", ".adb", ".ads", ".ada" } }, .{ .token = ";", .exts = &.{ ".clj", ".cljs", ".cljc", ".edn", ".el", ".lisp", ".scm", ".asm", ".s" } }, .{ .token = "%", .exts = &.{ ".erl", ".hrl", ".tex", ".cls", ".sty" } }, .{ .token = "!", .exts = &.{ ".f", ".for", ".ftn", ".f90", ".f95", ".f03", ".f08" } }, .{ .token = "\"", .exts = &.{ ".vim", ".vimrc" } }, }; /// What an armed search writes into the tag tail — and the ONLY record of /// which search it is: Enter reads the marker back (submitSearch) instead of /// pardes carrying a second piece of pane state per command. The pattern is /// everything past the `/`, so a pattern may itself contain slashes; the word /// before it is the builtin's own name, so the armed tag reads as the command /// it will run. The bare `/` has no name — it searches the pane itself. pub const search_marker = " /"; pub const find_marker = " Find /"; pub const grep_marker = " Grep /"; pub const rename_marker = " Rename /"; pub const symbol_marker = " WsSymbols /"; /// helix `s` / `S`. The only two markers whose word is NOT a builtin — there /// is no Select/Split command to run from a tag, they name the key that armed /// the input so the tag still reads as what it is about to do. They also mark /// the one input that previews as you type (pardes.zig, previewSelRegex). pub const select_marker = " Select /"; pub const split_marker = " Split /"; /// Output-buffer names (acme's +Errors). Cosmetic now, and deliberately so: a /// buffer is DERIVED from the command that opened it (output_pane.traits), and /// nothing identifies one by matching this text any more — renaming any of /// these changes only what you read in a tag. pub const search_buffer = "+Search"; pub const help_buffer = "+Help"; pub const jumps_buffer = "+Jumps"; pub const themes_buffer = "+Themes"; pub const fonts_buffer = "+Fonts"; pub const hover_buffer = "+Hover"; pub const lsp_buffer = "+Lsp"; // ============================================================================ // PART 3 — THE HELIX KEYMAP. READ THIS BEFORE RETARGETING ANYTHING BELOW. // // These are not free choices. `zig build hxdiff` (360 cases) and // `zig build hxparity` (440 cases) are DIFFERENTIAL suites: they drive a real // helix and this core with the same keystrokes and compare the results, and a // mismatch is a failure, not a diff to accept. Moving a key here therefore // breaks the build until the divergence is written down as a waiver with a // reason — which is the correct workflow for a DELIBERATE divergence (Alt-c // above is one) and an alarm for an accidental one. // // Everything in PART 1 is outside that contract and free to move. // ============================================================================ // ---- modal prefixes ---- // These are STORED — pane.pending / pending2 / find_op hold the codepoint // ITSELF until the next key completes the sequence, and the continuation reads // it back — so they are bare codepoints rather than Chords, and pardes.zig // matches them with `isPrefix` instead of `hit`. A prefix must therefore be an // unmodified printable key. Untyped so they compare against both the u21 and // the u8 fields that hold them. pub const goto_prefix = 'g'; pub const view_prefix = 'z'; pub const match_prefix = 'm'; pub const replace_prefix = 'r'; pub const next_prefix = ']'; pub const prev_prefix = '['; // f/F/t/T: the stored byte IS the operator — `f`/`t` mean forward and `t`/`T` // mean stop short — so the four are read back as values, not just matched. pub const find_char_fwd = 'f'; pub const find_char_back = 'F'; pub const till_char_fwd = 't'; pub const till_char_back = 'T'; /// repeat the last f/F/t/T pub const repeat_find: []const Chord = &.{.{ .cp = '.', .alt = true }}; // ---- motion ---- pub const move_left: []const Chord = &.{ .{ .cp = 'h' }, .{ .cp = Key.left } }; pub const move_right: []const Chord = &.{ .{ .cp = 'l' }, .{ .cp = Key.right } }; pub const move_down: []const Chord = &.{ .{ .cp = 'j' }, .{ .cp = Key.down } }; pub const move_up: []const Chord = &.{ .{ .cp = 'k' }, .{ .cp = Key.up } }; pub const next_word_start: []const Chord = &.{.{ .cp = 'w' }}; pub const prev_word_start: []const Chord = &.{.{ .cp = 'b' }}; pub const next_word_end: []const Chord = &.{.{ .cp = 'e' }}; pub const next_long_word_start: []const Chord = &.{.{ .cp = 'W' }}; pub const prev_long_word_start: []const Chord = &.{.{ .cp = 'B' }}; pub const next_long_word_end: []const Chord = &.{.{ .cp = 'E' }}; pub const line_start: []const Chord = &.{ .{ .cp = '0' }, .{ .cp = Key.home } }; pub const line_end: []const Chord = &.{ .{ .cp = '$' }, .{ .cp = Key.end } }; pub const line_first_nonws: []const Chord = &.{.{ .cp = '^' }}; /// helix goto_line: only acts WITH a count (bare G is a no-op; `ge` is /// goto-last-line) pub const goto_line: []const Chord = &.{.{ .cp = 'G' }}; /// grapheme motion in a ONE-LINE context (a tag, the topbar): the arrows only. /// A tagline spends h/l on the layout (window_keys) and the topbar answers /// them itself (topbar_left/right), so the letters are not in this vocabulary. pub const line_move_left: []const Chord = &.{.{ .cp = Key.left }}; pub const line_move_right: []const Chord = &.{.{ .cp = Key.right }}; // ---- paging and the view ---- pub const half_page_down: []const Chord = &.{.{ .cp = 'd', .ctrl = true }}; pub const half_page_up: []const Chord = &.{.{ .cp = 'u', .ctrl = true }}; pub const page_down: []const Chord = &.{ .{ .cp = 'f', .ctrl = true }, .{ .cp = Key.page_down } }; pub const page_up: []const Chord = &.{ .{ .cp = 'b', .ctrl = true }, .{ .cp = Key.page_up } }; /// under `z`: put the cursor's line at the top / centre / bottom of the view pub const view_top: []const Chord = &.{.{ .cp = 't' }}; pub const view_center: []const Chord = &.{ .{ .cp = 'z' }, .{ .cp = 'c' } }; pub const view_bottom: []const Chord = &.{.{ .cp = 'b' }}; /// under `z`: scroll the view one line, cursor snapped to the scrolloff edge pub const view_scroll_down: []const Chord = &.{ .{ .cp = 'j' }, .{ .cp = Key.down } }; pub const view_scroll_up: []const Chord = &.{ .{ .cp = 'k' }, .{ .cp = Key.up } }; // ---- under `g` (goto) ---- pub const goto_file_start: []const Chord = &.{.{ .cp = 'g' }}; pub const goto_last_line: []const Chord = &.{.{ .cp = 'e' }}; pub const goto_line_start: []const Chord = &.{.{ .cp = 'h' }}; pub const goto_line_end: []const Chord = &.{.{ .cp = 'l' }}; pub const goto_first_nonws: []const Chord = &.{.{ .cp = 's' }}; pub const goto_line_down: []const Chord = &.{.{ .cp = 'j' }}; pub const goto_line_up: []const Chord = &.{.{ .cp = 'k' }}; pub const goto_column: []const Chord = &.{.{ .cp = '|' }}; /// view-relative rows (helix goto_window) pub const goto_view_top: []const Chord = &.{.{ .cp = 't' }}; pub const goto_view_center: []const Chord = &.{.{ .cp = 'c' }}; pub const goto_view_bottom: []const Chord = &.{.{ .cp = 'b' }}; // helix's five LSP gotos, all under `g` and nowhere else. They are motions, // not builtins — a motion has no business being a word you can middle-click, // which is why they are not in the language group under `SPC l`. pub const goto_definition: []const Chord = &.{.{ .cp = 'd' }}; pub const goto_declaration: []const Chord = &.{.{ .cp = 'D' }}; pub const goto_type_definition: []const Chord = &.{.{ .cp = 'y' }}; pub const goto_implementation: []const Chord = &.{.{ .cp = 'i' }}; pub const goto_references: []const Chord = &.{.{ .cp = 'r' }}; // ---- under `m` (match) ---- /// `mm` acts at once, so it is an ordinary chord pub const match_bracket: []const Chord = &.{.{ .cp = 'm' }}; // The other five are SUB-prefixes: each waits for a textobject or surround // character (`mi(`, `mr[{`), so pardes stores them the way it stores `m` and // they are bare codepoints for the same reason as the block above. pub const match_inside = 'i'; pub const match_around = 'a'; pub const surround_add = 's'; pub const surround_replace = 'r'; pub const surround_delete = 'd'; // ---- under `]` / `[` ---- pub const goto_paragraph: []const Chord = &.{.{ .cp = 'p' }}; pub const add_newline: []const Chord = &.{.{ .cp = ' ' }}; /// step the diagnostics list, asking for one if it is not up yet pub const goto_diagnostic: []const Chord = &.{.{ .cp = 'd' }}; /// ]D / [D — the last / the first pub const goto_diagnostic_end: []const Chord = &.{.{ .cp = 'D' }}; // ---- insert entry ---- pub const insert: []const Chord = &.{.{ .cp = 'i' }}; pub const append: []const Chord = &.{.{ .cp = 'a' }}; pub const insert_line_start: []const Chord = &.{.{ .cp = 'I' }}; pub const insert_line_end: []const Chord = &.{.{ .cp = 'A' }}; pub const open_below: []const Chord = &.{.{ .cp = 'o' }}; pub const open_above: []const Chord = &.{.{ .cp = 'O' }}; // ---- selection ---- pub const select_mode: []const Chord = &.{.{ .cp = 'v' }}; pub const select_line: []const Chord = &.{.{ .cp = 'x' }}; pub const select_line_bounds: []const Chord = &.{.{ .cp = 'X' }}; pub const shrink_to_line_bounds: []const Chord = &.{.{ .cp = 'x', .alt = true }}; pub const collapse_selection: []const Chord = &.{.{ .cp = ';' }}; pub const flip_selection: []const Chord = &.{.{ .cp = ';', .alt = true }}; pub const select_all: []const Chord = &.{.{ .cp = '%' }}; // ---- multiple cursors ---- // // helix's Selection is a LIST of ranges with a primary index, and these ten // keys are the ones that act on the list rather than on the text: every other // key is replayed once per range instead (pardes.zig, replaySels). Alt-C is // Alt-SHIFT-c and so does not collide with pane_to_new_column's Alt-c — `hit` // compares the codepoint, and `C` is `C`. pub const copy_sel_below: []const Chord = &.{.{ .cp = 'C' }}; pub const copy_sel_above: []const Chord = &.{.{ .cp = 'C', .alt = true }}; pub const keep_primary_sel: []const Chord = &.{.{ .cp = ',' }}; pub const remove_primary_sel: []const Chord = &.{.{ .cp = ',', .alt = true }}; pub const rotate_sel_fwd: []const Chord = &.{.{ .cp = ')' }}; pub const rotate_sel_back: []const Chord = &.{.{ .cp = '(' }}; pub const split_sel_newline: []const Chord = &.{.{ .cp = 's', .alt = true }}; pub const merge_sels: []const Chord = &.{.{ .cp = '-', .alt = true }}; pub const merge_consecutive_sels: []const Chord = &.{.{ .cp = '_', .alt = true }}; pub const trim_sels: []const Chord = &.{.{ .cp = '_' }}; // The other two list-making keys: a REGEX turns each range into many. Both // arm the tag input above (select_marker / split_marker) instead of doing // anything immediately, so `s` and `S` are the only normal-mode keys whose // effect lands a keystroke later, on Enter — or live, as you type. // `s` is free here despite `gs` (goto_first_nonws) also being `s`: a pending // prefix is matched by the stored codepoint, never by these chords. pub const select_regex: []const Chord = &.{.{ .cp = 's' }}; pub const split_regex: []const Chord = &.{.{ .cp = 'S' }}; // ---- edits ---- pub const delete: []const Chord = &.{.{ .cp = 'd' }}; pub const delete_noyank: []const Chord = &.{.{ .cp = 'd', .alt = true }}; pub const change: []const Chord = &.{.{ .cp = 'c' }}; pub const yank: []const Chord = &.{.{ .cp = 'y' }}; pub const replace_with_yank: []const Chord = &.{.{ .cp = 'R' }}; /// helix: the DEFAULT register, not the system clipboard — most terminals /// refuse the OSC 52 read, so a round trip would never come back pub const paste_after: []const Chord = &.{.{ .cp = 'p' }}; pub const paste_before: []const Chord = &.{.{ .cp = 'P' }}; pub const switch_case: []const Chord = &.{.{ .cp = '~' }}; pub const to_lowercase: []const Chord = &.{.{ .cp = '`' }}; pub const to_uppercase: []const Chord = &.{.{ .cp = '`', .alt = true }}; pub const join_lines: []const Chord = &.{.{ .cp = 'J' }}; pub const indent: []const Chord = &.{.{ .cp = '>' }}; pub const unindent: []const Chord = &.{.{ .cp = '<' }}; /// helix's format_selections — its neighbour on the keyboard and in the keymap pub const format: []const Chord = &.{.{ .cp = '=' }}; pub const increment: []const Chord = &.{.{ .cp = 'a', .ctrl = true }}; pub const decrement: []const Chord = &.{.{ .cp = 'x', .ctrl = true }}; pub const undo: []const Chord = &.{.{ .cp = 'u' }}; pub const redo: []const Chord = &.{.{ .cp = 'U' }}; /// helix `toggle_comments`: comment or uncomment every line the selection /// touches, with `comment_tokens` above choosing the token. Ctrl-c reaches a /// terminal pane only in NORMAL mode — raw tty forwards it to the program, /// where it is still SIGINT. pub const comment_toggle: []const Chord = &.{.{ .cp = 'c', .ctrl = true }}; /// In body normal mode, clear modal residue and run Toggleterm (the same /// builtin as `SPC w t`). Elsewhere: leave insert mode; abandon a leader path, /// tag, armed search or the topbar; raw tty mode forwards it to the program. pub const escape: []const Chord = &.{.{ .cp = Key.escape }}; // ---- insert mode ---- // The three helix aliases: normalized to the base key and re-dispatched, so // they behave identically to it everywhere downstream. pub const insert_backspace_alias: []const Chord = &.{.{ .cp = 'h', .ctrl = true }}; pub const insert_enter_alias: []const Chord = &.{.{ .cp = 'j', .ctrl = true }}; pub const insert_delete_alias: []const Chord = &.{.{ .cp = 'd', .ctrl = true }}; /// helix insert-mode kills. Ctrl-w is also the WINDOW prefix in normal/tty — /// insert mode wins it, which is helix's own arrangement. pub const delete_word_backward: []const Chord = &.{ .{ .cp = 'w', .ctrl = true }, .{ .cp = Key.backspace, .alt = true } }; pub const delete_word_forward: []const Chord = &.{ .{ .cp = 'd', .alt = true }, .{ .cp = Key.delete, .alt = true } }; pub const kill_to_line_start: []const Chord = &.{.{ .cp = 'u', .ctrl = true }}; pub const kill_to_line_end: []const Chord = &.{.{ .cp = 'k', .ctrl = true }};