//! 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 = '?'; /// SPC leader: ONE key path per builtin, the whole remapping surface. An /// EnumArray, so a new enum field is a compile error until someone has DECIDED /// its path. 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 = std.EnumArray(Builtin, ?[]const u8).init(.{ .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 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 tag prefix's mode word — live chrome, not text you own pub const tag_normal = "NOR"; pub const tag_insert = "INS"; pub const tag_tty = "TTY"; 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; // ---- 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 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 = ':'; /// `@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 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 /"; /// 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 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 = '%' }}; // ---- 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' }}; /// Exit select mode / drop pending prefixes and the count; leave insert mode; /// abandon a tag, an armed search or the topbar. On an OUTPUT buffer there is /// nothing worth keeping, so Esc there is the Del builtin instead. 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 }};