# Configuration [README](../README.md) Startup configuration, preferences, bindings, and opt-in native language servers. See the [example configuration](../examples/config.rb) and [plugin API](plugins.md). By default, vamp loads `$XDG_CONFIG_HOME/vamp/config.rb`, or `~/.config/vamp/config.rb` when that environment variable is unset. `--config path` selects a file explicitly; `--no-config` bypasses Ruby entirely. Set `VAMP_RUBY` to select a different Ruby executable. Rejected configuration opens a centered floating **Configuration error** window, sized like the floating file explorer. It shows the selected config path, full failure reason, and explains that default settings and bindings are in use. The diagnostic remains visible even if background messages arrive; editing keys and paste are ignored until **Enter** or **Escape** dismisses it. Use arrows (or Ctrl+h/j/k/l) to scroll long paths/reasons horizontally or vertically, and **Home** to reset the view. Correct the config and restart vamp to apply it. Invalid individual preferences/bindings that do not reject the whole configuration still report ordinary notices. ```ruby # @param config [Vamp::Config] Vamp.configure do |config| config.leader = "space" config.clipboard_mode = "system" config.git_blame = true # false disables current-line attribution config.git_gutter = "bar" # or "symbols", "line_numbers", "none" config.scroll_distance = 0 # nonnegative edge distance, or nil to center config.highlight_trailing_whitespace = false # true marks line-end whitespace in red config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" } config.whitespace_scope = "all" # or "leading" for indentation only config.tab_spaces = nil # opt in with a fixed number of spaces per Tab, e.g. 4 config.language_tab_spaces = {} # overrides: { "Ruby" => 2, "Makefile" => nil } config.file_explorer_side = "right" config.file_explorer_view = "tree" config.file_picker_view = "tree" config.diff_context_lines = 10 config.diff_expand_lines = 15 config.diff_view_mode = "split" # or "unified" config.diff_split_orientation = "vertical" # or "horizontal" config.diff_follow_working_copy = true config.bind "ctrl+h", config.core.left, mode: config.modes.tree config.bind "ctrl+l", config.core.right, mode: config.modes.tree config.plugin "plugins/greeting.rb" config.bind "g", "greet" config.bind "leader g", "greet" config.bind "leader q", config.core.quit config.bind "ctrl+r", config.core.redo config.bind "shift+j", config.core.join_lines config.bind "ctrl+c", config.core.normal, mode: config.modes.insert config.bind "ctrl+f", config.core.search config.bind "ctrl+c", config.core.normal, mode: config.modes.search config.bind "ctrl+v", config.core.select config.bind "ctrl+c", config.core.copy, mode: config.modes.visual config.bind "ctrl+x", config.core.cut, mode: config.modes.visual config.bind "ctrl+r", config.core.change, mode: config.modes.visual end ``` ## Preferences Plugin paths resolve relative to the config file, not the shell's working directory. `config.bind key, target, mode: config.modes.normal` overrides one key or a whitespace-separated key sequence; editing modes are `config.modes.normal`, `config.modes.insert`, `config.modes.command`, `config.modes.search`, and `config.modes.visual`. Sequences require Normal/Visual. `config.modes.selection` configures shared list navigation; `config.modes.tree` overrides it in tree browsing. Omitting `mode` selects Normal. The leader defaults to `"space"` and is applied before configured bindings. `config.file_explorer_side` defaults to `"right"`; other choices are `"left"`, `"top"`, and `"bottom"`. Independent `file_explorer_view` / `file_picker_view` preferences both default to `"tree"` and accept `"list"` as an alternative. Invalid preference strings report notices and retain defaults while valid bindings still install. Non-string/empty values disable the whole configuration like other Ruby type errors. `config.clipboard_mode` accepts `"system"` (default) or `"separate"`. An invalid policy string reports a notice and retains System while valid bindings still install; non-string/empty values reject the whole Ruby configuration. `config.scroll_distance` defaults to `0`: keep the viewport still until its primary cursor or selected picker entry would leave it. A nonnegative integer reserves that many visible rows above/below the cursor, and terminal-cell columns left/right in unwrapped text. `nil` keeps it centered; integers at least half the current viewport dimension behave the same way until resizing makes that margin usable. The start of content limits centering, while trailing blank space allows centering near the end. Whole two-row picker entries stay aligned, using the nearest entry to the center when exact margins cannot fit. The setting applies to ordinary and historical buffers, split/unified diff panes, file pickers and explorers, command/buffer palettes, completion lists, and revision selectors. Wrapped text measures visual rows, not source lines; picker distances use result rows, excluding borders, search fields, and help. Manual mouse-wheel scrolling remains detached until keyboard navigation resumes. Only `nil` or integers from `0` through the platform's Rust `usize` maximum (`18446744073709551615` on 64-bit systems) are accepted; other values reject the configuration. `config.highlight_trailing_whitespace` defaults to `false`. Set it to `true` to give trailing whitespace a red background in ordinary and historical buffers and both sides of split/unified diffs, including wrapped and horizontally scrolled text. Spaces, expanded tab cells, and Unicode whitespace at the end of a source line are highlighted; whitespace-only lines are included, but empty lines and padding beyond the text are not. Wrapping does not make interior whitespace trailing. Search, selections, and secondary carets retain their background precedence; diagnostic underlines remain visible. This changes display only, not file contents. Only literal Ruby booleans are accepted; other values reject the configuration. `config.whitespace_characters` defaults to `{}` (disabled). Assign a hash mapping literal whitespace characters to dim display markers, for example `{ " " => "·", "\t" => "→", "\u00a0" => "⍽", "\n" => "↵" }` for spaces, tabs, nonbreaking spaces, and line breaks. Unlisted characters keep their ordinary appearance. Keys and values must be single-character strings; keys must be whitespace with visible terminal width, tabs, or `"\n"`, and values must be printable one-cell characters. Carriage returns and other control characters, zero-width markers, and wide markers reject the configuration. Only indicator cells receive terminal dim styling; source-text colors, diagnostic underlines, and highlighted backgrounds remain intact. `config.whitespace_scope` defaults to `"all"`. Use `"leading"` to mark only the initial whitespace prefix of each source line; a whitespace-only line is entirely leading, but wrapping and horizontal scrolling do not create new indentation. Only the strings `"all"` and `"leading"` are accepted; other values reject the configuration. A `"\n"` map entry marks actual line breaks independently of `whitespace_scope`, including blank lines and a file's final newline when present. Soft wraps, missing final newlines, and the virtual empty EOF row do not gain markers. The glyph occupies the existing end-of-line caret cell on the final wrapped segment; a full-width line uses the existing extra caret segment. In unwrapped text, an offscreen end-of-line marker is clipped like other source cells. Newline markers inside syntax-recognized strings are suppressed along with horizontal whitespace markers. Whitespace inside strings recognized by the bundled syntax grammar stays literal, including raw/multiline strings, heredocs, interpolated strings, and strings with embedded-language highlighting. Outside strings, configured markers still apply. String boundaries come from the original source, so wrapping, horizontal scrolling, and split/unified historical diffs retain this distinction. Plain text and unknown languages have no syntax-defined string boundaries; quotes are not guessed to be strings. Markers appear in ordinary and historical buffers and both sides of split/unified diffs. Tabs and wide Unicode whitespace show the marker in their first source cell and leave remaining cells blank, preserving tab stops, wrapping, horizontal scrolling, and cursor/selection geometry. A clipped first cell hides its marker; padding beyond the source does not gain markers. Whitespace with combining characters remains intact. Syntax colors, search, selections, diagnostics, and trailing-whitespace highlighting still apply. This changes display only: editing, copying, and saving retain the original characters. `config.tab_spaces` defaults to `nil`, preserving literal tabs. Set an integer from `1` through `255` to insert that many spaces per Tab in Insert mode, at every caret. This is a fixed count, not alignment to the next tab stop; it does not change how existing tab characters are displayed. `config.language_tab_spaces` defaults to `{}`. It overrides the global setting for the active buffer's detected syntax language, using the case-sensitive names shown in the language bar, for example `{ "Rust" => 4, "Ruby" => 2, "Python" => 4, "Makefile" => nil }`. A `nil` override preserves literal tabs for that language, even when the global setting uses spaces. Unlisted or unknown languages and unnamed buffers without a recognized shebang use `tab_spaces`. Detection reuses syntax highlighting's filename, extension, and shebang rules, independently of whether an LSP is enabled. Active snippets retain Tab/Shift+Tab argument navigation; after the final stop, Tab uses the configured insertion setting. Explicit Tab bindings still take precedence, and Ctrl+i remains a Tab alias. Bracketed paste, clipboard paste, completion payloads, and plugin text retain literal tab characters. Insert undo/redo and dot repeat preserve the resolved spaces or tab. Widths other than `nil` or integers in `1..255`, non-hash language overrides, and empty, non-string, or control-containing language keys reject the configuration. `config.git_blame` defaults to `true`; set it to `false` to disable current-line inline Git blame and its background worker. Only literal Ruby booleans are accepted; other values reject the configuration. Toggle it for the running session with `:git_blame.toggle`, its palette action, or a custom binding such as `config.bind "leader b", config.core.git_blame.toggle`. There is no default shortcut. `config.git_gutter` defaults to `"bar"`: a fat `▌` with one blank column between the line number and marker and another before the text. `"symbols"` uses `+` / `-` / `~` with the same spacing. Added, removed, and modified lines use green, red, and yellow respectively. `"line_numbers"` colors the existing numbers without adding a marker column; the current line retains its cyan/bold caret highlight. `"none"` hides line-gutter repository indicators, not line numbers or picker file statuses. Other modes retain diagnostic number colors independently of repository markers. Unsupported strings, empty strings, and non-string values reject the configuration. In `"line_numbers"` mode, color precedence is **current-line cyan/bold → highest-severity diagnostic → repository change → default number color**. In bar/symbol modes, diagnostic number colors and repository markers remain independent. Indicators compare the current editor text (including unsaved edits) against Git `HEAD`, or the parent `@-` in Git-backed jj repositories, rather than against the last save or Git index. Line endings use the editor's LF normalization. They refresh after edits/saves, on focus regain, and once per second for repository changes. Historical snapshots do not receive live indicators. Eligible named files not yet on disk show nonempty unsaved text as added; tracked files missing on disk still compare their buffer text against the baseline. Non-repository, ignored, binary, symlink, and unsupported files have no indicators; neither do jj working changes with multiple parents. Both colocated and non-colocated Git-backed jj repositories are supported. Files above 16 MiB report an error and clear their indicators; request batches are limited to 64 MiB or 1024 files. `config.diff_context_lines = 10` sets how many unchanged aligned lines appear above and below each diff change; it defaults to `10` and accepts nonnegative integers, including `0` for changes only. `config.diff_expand_lines = 15` sets how many additional lines each directional expansion reveals per keypress; it defaults to `15` and accepts positive integers. Both are bounded by the platform's Rust `usize` maximum (`18446744073709551615` on 64-bit systems). Floats, strings, booleans, negative or oversized values, and a zero expansion step reject the whole Ruby configuration. `config.diff_view_mode` selects the initial view for each new comparison: `"split"` (default) or `"unified"`. `config.diff_split_orientation` selects `"vertical"` (left/right, default) or `"horizontal"` (above/below) for split views. Invalid values reject the configuration. Normal-mode **Space v** cycles split/unified; **Space s** flips split orientation. These controls affect the current comparison, not startup defaults, and retain their choices across file switches and tabs. Orientation changes apply only in split view. `config.diff_follow_working_copy` defaults to `true`. A Working-checkout After follows descendant checkouts automatically, including in inactive tabs; `false` disables endpoint following without disabling ordinary live-buffer editing or reload. Only literal booleans are accepted. Pinned After endpoints never move, but their checkout-aware editability checks remain enabled regardless of this setting. See [repository diff](editing.md#repository-diff) for ancestry and refresh behavior. ```ruby config.bind "ctrl+b", config.core.diff.mode.toggle config.bind "leader v", config.core.diff.view.toggle config.bind "leader s", config.core.diff.orientation.toggle ``` ### Top and bottom bars `config.bar.top` and `config.bar.bottom` each accept ordered `left`, `center`, and `right` arrays of segment names. Both bars accept every builtin and custom segment, with the same anchors, title templates, margins, padding, styled edges, and collision handling. Left and right groups anchor to their respective edges; the center group is centered on the whole row, not on the gap remaining between the other groups. Array order is display order within each group. Omitted groups and segment options retain that bar's defaults; an empty array removes that group. Use the two setters independently; a combined `config.bar = { ... }` assignment is not supported. Defaults put `left: [:tabs]` and `right: [:battery]` in the top bar; the bottom has `left: [:mode, :pending, :buffer, :diagnostics, :path, :selections, :count]` and `right: [:language, :lsp, :position]`; other groups are empty. Set the bars independently, as in this swapped layout with status information above and tabs plus live battery charge below: ```ruby Vamp.configure do |config| config.bar.top = { left: [:mode, :path, :diagnostics, :selections, :count, :pending, :buffer], center: [], right: [:language, :lsp, :position], mode: { padding: 1, leading_edge: "", trailing_edge: "▓░" }, position: { padding: 1, leading_margin: 1, trailing_margin: 0, leading_edge: "░▓", trailing_edge: "" } } config.bar.bottom = { left: [:tabs], center: [], right: [:battery], tabs: { title: "{index} {view}{name}{modified}", padding: 1, leading_edge: "░▓", trailing_edge: "▓░", first_leading_edge: "" }, battery: { padding: 1, leading_edge: "░▓", trailing_edge: "" } } end ``` Builtins are `:tabs`, `:battery`, `:mode`, `:path`, `:position`, `:language`, `:lsp`, `:diagnostics`, `:selections`, `:count`, `:pending`, and `:buffer`, in either bar. They provide tab titles, battery charge, mode, file path/dirty state, caret position, detected language, ready LSP server names, diagnostics, selection count, numeric prefix, pending key sequence, and buffer identity/count. A bar with no visible content consumes no row. For example, `config.bar.top = { left: [], center: [], right: [] }` removes the top bar. A bar whose only visible content is `:tabs` is hidden with a single tab, whether above or below the workspace. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped. Commands and buffer searches use floating input; operation feedback uses a bottom-right popup. Neither reserves a footer row. Each segment's `title` is a literal template supporting only `{index}`, `{view}`, `{name}`, `{path}`, and `{modified}`: the one-based tab index, comparison-view marker, filename, file path, and dirty marker (`" [+]"` when dirty, `""` when clean). `{view}` expands to `"Diff · "` when the tab owns a comparison or revision selector, otherwise `""`. Tabs use each tab's view and buffer metadata; other segments use the active context. Tabs default to `"{index} {view}{name}{modified}"`; custom templates without `{view}` retain their output. An empty title on another segment uses its builtin content or named label value. Templates are not Ruby code and never invoke a render callback. `:path` and `{path}` abbreviate the actual home-directory prefix to `~/` (`~` for the home directory itself). This is display-only: save paths, buffer identity, and LSP file URIs retain the original path. Sibling directories with similar names are not abbreviated. `:lsp` displays only ready servers assigned to the focused buffer, in configured stack order joined with ` + `. Disabled, initializing, failed, or unassigned servers do not appear. Names come from the initialization reply's `serverInfo.name`, falling back to the configured executable basename; no arbitrary named label can make an inactive server appear active. `margin` adds unstyled space outside both edges of a segment, unlike the styled internal `padding`. `leading_margin` and `trailing_margin` override their respective sides. All accept integers from `0` through `64`. Margins default to zero except the bottom bar's `:position`, which has one leading column and no trailing margin. LSP has no default margins. For example, `config.bar.bottom = { position: { leading_margin: 0 } }` removes position's external spacing without changing its padding. Margins count toward anchor placement and collision handling; empty segments consume no margin, and margins yield along with padding and edges when only clipped content fits. `padding` adds spaces inside the segment's styling, on both sides of its title, and accepts integers from `0` through `64` (default `1`). `leading_edge` and `trailing_edge` are separate styled glyph strings, not padding; `""` removes an edge. Tabs default to `"░▓"` / `"▓░"` edges; mode has only the trailing `"▓░"` edge, position and battery only the leading `"░▓"` edge, and other segments have none. `first_leading_edge` optionally overrides the first occurrence's leading glyphs: for tabs this is the actual first tab, not the first tab remaining after clipping; other segments have one occurrence. Tabs default this override to `""`, suppressing the first tab's left fade. Unicode widths are measured in terminal cells. Edge strings must be single-line and contain no control characters. Unknown style keys, invalid templates, and wrong types reject configuration. Any other segment name is a custom named value, such as `:build_status`. Set initial strings with `config.labels`; an empty value hides the custom segment, including its title override, margins, padding, and edges. Names match `[A-Za-z_][A-Za-z0-9_]*`. Labels are plain text: control characters are replaced rather than interpreted as terminal escapes. The same named value can appear in either bar. `:battery` displays `bat N%` from the OS, with an immediate background reading and refreshes every 30 seconds. macOS uses IOKit, Linux uses system battery sysfs entries, and Windows uses `GetSystemPowerStatus`; no shell commands run. Missing, unsupported, or unreadable batteries hide the segment. macOS/Linux currently report the first present internal/system battery; Windows uses the OS's system percentage. Move `:battery` to any anchor group or remove it from both bars to stop battery collection. A nonempty `config.labels[:battery]` or plugin value explicitly overrides the native text and pauses collection; clearing that value restores native readings. Other custom live values use [`editor.label(name, text)` or asynchronous plugin updates](plugins.md#named-label-updates). Configuration runs once at startup; there are no render callbacks. ### Command and search popup anchors Configure the two input windows independently: ```ruby Vamp.configure do |config| config.popup.command = { anchor: :top_right } config.popup.search = { anchor: :center } end ``` Both default to `:bottom_left`. Supported anchors are `:top_left`, `:top_center`, `:top_right`, `:center_left`, `:center`, `:center_right`, `:bottom_left`, `:bottom_center`, and `:bottom_right`. String values and string hash keys are also accepted. Anchors refer to the chrome-free workspace, not the whole terminal or focused pane. Center positions use the full workspace width/height. The windows remain bounded to 72 columns by 3 rows, preserving titles, internal padding, cursor clipping, and modal behavior. This setting does not move revision selectors or the operation notification. Only the `anchor` option is supported; unknown anchors/options, callbacks, nulls, and wrong types reject configuration. Omitting a window or its anchor keeps its default. Use the two setters separately; a combined `config.popup = { ... }` assignment is not supported. ## Actions and modes `config.core` exposes the built-in actions below as immutable `Action` values; `config.modes` exposes immutable `Mode` values. Misspelled methods fail during configuration evaluation, and `bind` rejects the wrong value type. Built-in action strings, canonical Ex names (such as `"quit"`), shorthand aliases (such as `"q"`), and mode strings are not accepted. Only plugin-owned commands use exact string names (`"greet"`, without `:` or arguments) and can only be bound in Normal mode. Interactive Ex commands remain textual. Ruby provides runtime checks, not compile-time checking; the editor still validates action/mode compatibility. Registered command names may contain dot-separated segments, each starting with an ASCII letter followed by ASCII letters/digits/underscores/hyphens, plus one optional trailing `!`. Names cannot contain whitespace, empty segments, or leading/trailing dots. This grammar applies to core and plugin commands alike. For command-capable actions, canonical Ex names derive automatically from the action name without `core.`: `config.core.quit` corresponds to `:quit`, while `config.core.diff.close` corresponds to `:diff.close`. The single Rust alias table targets typed actions, so shorthand names such as `:q` follow the canonical name without repeating its spelling. Both canonical names and aliases are reserved against plugin registration; the Ruby ownership map comes from the same catalog and alias table. Native-only action eligibility is unchanged. ## Key notation Key notation follows [Herdr's readable style](https://herdr.dev/docs/configuration/#keybindings). Literal non-control Unicode characters are case-sensitive (`n`, `J`, `+`); named keys and modifiers are case-insensitive: - Named keys: `esc`, `enter`, `backspace`, `delete`, `left`, `right`, `up`, `down`, `home`, `end`, `tab`, `space`. - Modifiers: `ctrl+a` through `ctrl+z`, `alt+a` through `alt+z` (`option+` is an alias for `alt+`), `ctrl+alt+a` through `ctrl+alt+z` (either modifier order), `shift+a` through `shift+z`, and `shift+tab`. `shift+j` and `J` address the same key. Modified Backspace accepts `alt+backspace` / `option+backspace` and `cmd+backspace` / `super+backspace`. - Named punctuation: `minus`, `comma`, `ampersand`, `plus`, `backtick`. Each addresses the same binding as its literal character. `ctrl+i` aliases `tab`, and `ctrl+m` aliases `enter`, including inside sequences. Ctrl+h and Ctrl+j are distinct control keys, not Backspace/Enter aliases. ASCII letters support a single modifier or combined Ctrl+Alt/Option; Shift+Tab and the modified Backspace keys above are also supported. Cmd/Super character keys, other combined modifiers, other modified special keys, and Vim-style angle-bracket notation are rejected rather than silently treated as another key. Normal mode binds `ctrl+o` to `config.core.jump_back` and `ctrl+i`/`tab` to `config.core.jump_forward`. Insert-mode Tab inserts literal text outside snippets; inside one it moves to the next argument. Override or disable these keys with the ordinary per-mode binding interface; jump actions also appear in the command palette. `config.core.backspace_word` and `config.core.backspace_line` are typed actions eligible in Insert, Command, and Search. Ctrl+u defaults to `config.core.backspace_line` in all three modes on every platform; it can be rebound or disabled with the ordinary per-mode binding interface. macOS additionally defaults Option+Backspace and Command+Backspace to word and line deletion respectively. Any platform can explicitly bind these actions to supported keys; for example, `config.bind "ctrl+w", config.core.backspace_word, mode: config.modes.insert`. Word deletion follows Unicode word boundaries after skipping trailing whitespace; line deletion removes the caret-to-logical-line-start prefix, not the suffix or newline. Active snippets confine both actions to their argument. macOS prompt and filter queries also recognize these modified Backspace keys. Configuration accepts the Option/Alt and Command/Super Backspace names on every platform, but terminal modifier-event translation for these keys is macOS-only. Native modifier events must deliver Option or Super (Command in Kitty CSI-u); Meta is not Command. Ghostty's default Command+Backspace encoding is Ctrl+u (`\x15`), which invokes the same line-deletion action without a Ghostty override. It remains a distinct Ctrl+u binding, not a reconstructed Command modifier. Legacy ESC+DEL supports Option+Backspace without keyboard protocol negotiation. Normal mode uses literal `_` and `|` for above/below and left/right splits, and `alt+h/j/k/l` for directional pane focus. Bind the produced punctuation rather than a physical Shift combination. Close actions have no default binding; for example, `config.bind "leader x", config.core.pane.close`. See [panes and splits](editing.md#panes-and-splits) for shared-buffer and unsaved-change policies. Normal mode binds `ctrl+option+h/l` (`ctrl+alt+h/l`) to previous/next tab. Tab actions have no default open/close binding; for example, `config.bind "leader t", config.core.tab.new` and `config.bind "leader x", config.core.tab.close`. See [tabs](editing.md#tabs) for layout preservation and close guards. The built-in `gt`/`gb` sequences move to the first/last line in Normal and Visual; `gf` follows Markdown file links in Normal only. Bind `config.core.document_start`, `config.core.document_end`, or `config.core.follow_link` to another key or sequence to add another route. A configured/disabled `g`, a configured sequence starting with `g`, or choosing `g` as the leader overrides the built-in prefix in that binding mode. With a decimal prefix, `g` instead jumps immediately to that one-based source line: `42g` goes to line 42, clamped at EOF, at first nonblank. Normal moves all carets; Visual extends selections. In diff text panes, explicit line jumps reveal folded source lines rather than counting only displayed excerpts. `g` without a count still begins `gt`, `gb`, or `gf`; a count followed by `g` is complete and does not await a suffix. Rebind the Normal/Visual native action `config.core.go_to_line` to use the numeric prefix with another key. Existing `g` bindings and prefixes override this default route. Normal `yy` copies complete rows without moving the carets; `dd` / `cc` delete/change complete rows; `dw` / `db` delete forward/backward word spans within each caret's current logical line, preserving newlines. `cw` / `cb` cut the same spans and enter Insert. Leading and post-operator counts multiply and word edits stop at the line end/start. Bind `config.core.copy_line`, `config.core.delete_line`, `config.core.change_line`, `config.core.delete_word_forward`, `config.core.delete_word_backward`, `config.core.change_word_forward`, or `config.core.change_word_backward` to another key or sequence for another route. A configured/disabled operator key, a configured sequence starting with that key, or choosing it as the leader overrides its built-in operator prefix. These are dedicated actions, not general operator–motion grammar. Normal `C` / `D` / `Y` change/delete/copy the cursor-to-line-end suffix through `config.core.change_to_line_end`, `config.core.delete_to_line_end`, and `config.core.copy_to_line_end`. Normal and Visual `r` wait for a literal replacement character through `config.core.replace_character`: Normal honors the count; Visual replaces each selected grapheme without replacing newlines. Normal `s` deletes the current grapheme and enters Insert through `config.core.substitute_character`. Rebinding these actions preserves their native counts, Unicode boundaries, undo, and read-only guards. `f` / `F` seeks forward/backward on the current line in Normal and Visual, with counts such as `2fx`. Rebind `config.core.seek_forward` / `config.core.seek_backward` to a single key or leader sequence; the action then waits for a literal target character. For example, `config.bind "s", config.core.seek_forward` makes `2sx` seek the second following `x`. Pass `mode: config.modes.visual` to install a Visual route. These native-only actions do not have Ex commands. ## Action reference | Available modes | Methods on `config.core` | | --- | --- | | Normal | `insert`, `append`, `open_below`, `open_above` | | Normal, Visual | `insert_first_nonblank`, `append_line_end`, `clone_above`, `clone_below` | | Normal | `join_lines`, `undo`, `redo`, `command` | | Normal | `copy_line`, `delete_line`, `change_line` | | Normal | `change_to_line_end`, `delete_to_line_end`, `copy_to_line_end`, `substitute_character`, `delete_word_forward`, `delete_word_backward`, `change_word_forward`, `change_word_backward` | | Normal, Visual | `replace_character` | | Normal | `save`, `force_save`, `quit`, `force_quit`, `restart`, `force_restart`, `save_and_quit`, `force_save_and_quit`, `plugins` | | Normal | `edit`, `buffers`, `buffer`, `buffer_next`, `buffer_previous`, `buffer_delete`, `buffer_force_delete` | | Normal | `follow_link` | | Normal | `diff`, `diff.mode.toggle`, `diff.view.toggle`, `diff.orientation.toggle`, `diff.review.toggle` | | Normal, Insert, Visual, Command, Search | `diff.close` | | Normal, Insert, Visual, Command, Search | `word_wrap.toggle` | | Normal, Insert, Visual, Command, Search | `git_blame.toggle` | | Normal | `pane.split_horizontal`, `pane.split_vertical`, `pane.focus_left`, `pane.focus_down`, `pane.focus_up`, `pane.focus_right`, `pane.close`, `pane.force_close` | | Normal | `tab.new`, `tab.previous`, `tab.next`, `tab.close`, `tab.force_close` | | Normal | `jump_back`, `jump_forward` | | Normal | `lsp.definition`, `lsp.diagnostics`, `lsp.hover` | | Insert | `lsp.completion`, `snippet.next`, `snippet.previous` | | Normal | `search`, `paste_before`, `paste_after` | | Normal | `paste_system_before`, `paste_system_after` | | Normal, Visual | `select`, `select_linewise`, `search_next`, `search_previous` | | Normal, Visual | `repeat_last_edit` | | Normal, Visual | `file_picker`, `file_explorer` | | Normal, Visual | `document_start`, `document_end`, `go_to_line` | | Normal, Visual | `seek_forward`, `seek_backward` | | Normal, Insert, Visual | `line_start`, `line_end` | | Normal, Insert, Visual, Tree | `left`, `right` | | Normal, Insert, Visual, Selection, Tree | `down`, `up` | | Normal, Insert, Visual | `word_next`, `word_previous`, `word_end`, `big_word_next`, `big_word_previous`, `big_word_end` | | Normal, Insert | `delete` | | Visual | `copy`, `cut`, `change`, `select_lines`, `select_next`, `skip_occurrence`, `select_all` | | Visual | `copy_system` | | Normal, Insert, Command, Search, Visual | `normal`, `palette` | | Insert, Command, Search | `backspace` | | Insert | `newline` | | Command, Search | `submit` | `core.select` (`v`) and `core.select_linewise` (`V`) select characterwise and whole-row Visual shapes. Switching selectors retains endpoints; pressing the active shape's selector again cancels. Both use `config.modes.visual`; there is no separate linewise binding mode. `core.select_lines` (Visual Ctrl+l) still splits touched rows into Normal carets. `core.save` saves the current path; `core.save_and_quit` saves the active buffer and quits only if no buffer remains dirty. Both re-read disk text and compare its LF-normalized content with the saved baseline, refusing external changes or deletion with a warning. `core.force_save` and `core.force_save_and_quit` explicitly bypass that external-change check, but never historical-snapshot read-only protection, target-symlink rejection, or another loaded buffer's path ownership. Their canonical commands are `:force_save [path]` and `:force_save_and_quit [path]`, with aliases `:w!` and `:wq!`; guarded `:save [path]` / `:w` and `:save_and_quit [path]` / `:wq` remain unchanged. A new buffer opened at a nonexistent path may save normally. Configured force-save actions act immediately, without confirmation. Live clean buffers reload external changes automatically, including inactive tabs/panes, using 250 ms metadata checks and reading content only when metadata changes. Historical snapshots are excluded; dirty buffers keep their edits. Reload clears stale undo/redo, preserves and clamps view positions, and invalidates outdated annotations/requests. Missing, unreadable, or invalid changed files preserve text and report a warning. This polling does not replace the actual-content recheck on save. `core.quit` refuses unsaved changes in any buffer. `core.edit` and `core.buffer` open prefilled Ex prompts for their required path or buffer ID; deletion actions target the current buffer. Configured `core.force_quit`, `core.buffer_force_delete`, `core.pane.force_close`, and `core.tab.force_close` keys act immediately, without confirmation. `config.core.buffers` opens the searchable buffer picker in Normal mode, also available as `:buffers` and default **Space b l**. Its navigation uses `config.modes.selection`, like the command palette. For example, `config.bind "leader b b", config.core.buffers` adds another opening shortcut. Search, acceptance, and cancellation are described under [Buffers](editing.md#buffers). `core.restart` and `core.force_restart` have no default keybindings. Their no-argument commands are `:restart` and `:force_restart`, with `:restart!` as an alias for force restart. Normal restart closes clean buffers across every tab, pane, and historical view; any dirty buffer blocks the relaunch with a warning, retains its text and undo/redo history, and leaves the editor interactive without restoring the clean buffers already closed. Force restart discards unsaved text without writing it. The palette confirms force restart, while a configured `config.core.force_restart` binding executes directly. Restart relaunches the same executable with the original CLI arguments after full session teardown. Raw mode and input reporting briefly reset, but the alternate screen remains active across the process handoff; the new process resumes input modes and clears and repaints the screen atomically in its first frame. It rereads configuration and the original startup file (or opens an unnamed buffer when no file was supplied), rather than restoring current buffers, tabs, views, or session state. Final quit, errors, and panics restore normal input modes, the normal screen, cursor visibility, and the prior panic hook. ## Binding overrides and failures Adding a binding keeps the original key. Pass `nil` to disable a key, including literal typing in Insert/Command mode: ```ruby Vamp.configure do |config| config.bind "n", config.core.insert config.bind "i", nil config.bind "space", config.core.command config.bind ":", nil end ``` The last valid declaration for a key/mode wins. Invalid keys, incompatible action/mode pairs, ambiguous sequence prefixes, or unknown command names leave that binding unchanged and appear in the operation notification; other valid bindings still load. An invalid leader key reports a notice and retains Space, while valid bindings still install. Ruby syntax errors, unknown methods, and type errors—including non-string or empty leader values—disable the entire configuration and fall back to defaults. One plugin's load failure does not prevent other plugins loading. Disabling a plugin removes its single-key and sequence bindings, without restoring any default keys they replaced. Bindings are resolved inside the editor module, not the terminal adapter. Bracketed paste remains literal and bypasses keybindings. If configuration disables your keys for leaving Insert/Command/Search/Visual mode or entering commands, restart with `--no-config` to restore defaults. ## Selection-list bindings The palette, floating file picker, focused docked explorer, and diff selectors/navigation share `config.modes.selection`. It is a binding context, not a buffer editing mode. `config.core.down` moves to the next list item (or scrolls aligned diff rows); `config.core.up` moves to the previous. Defaults are Ctrl+j/Down/Ctrl+n for next and Ctrl+k/Up for previous. Remap or disable any single key: ```ruby config.bind "ctrl+f", config.core.down, mode: config.modes.selection config.bind "ctrl+b", config.core.up, mode: config.modes.selection config.bind "ctrl+j", nil, mode: config.modes.selection ``` Selection bindings take precedence while a file or command list is focused. Unbound printable keys filter in List/explorer INSERT, but not explorer NORMAL; a disabled key is consumed without filtering or moving. Only `up`, `down`, and `nil` are accepted—no leader sequences, edits, or plugin commands. Invalid declarations leave existing routes unchanged. Navigation does not change text, carets, history, or the buffer mode. Both list footers show effective keys, including remaps and disabled directions. Tree views use `config.modes.tree`, accepting `up`, `down`, `left`, `right`, or `nil`. Explorer NORMAL defaults to h/j/k/l for parent/next/previous/child, with the existing control/arrow routes retained. Tree routes override shared selection routes. In explorer INSERT, printable tree keys are not intercepted: h/j/k/l become query text, while control/special-key tree routes still navigate. Ctrl+h/l select visible parents/children in the filtered projection without changing the query or saved folder expansion. Unmatched keys inherit shared selection bindings. Cached hints prioritize plain keys in NORMAL and omit them in INSERT. For example: ```ruby config.bind "ctrl+f", config.core.right, mode: config.modes.tree config.bind "ctrl+h", nil, mode: config.modes.tree ``` The diff's changed-file explorer and floating picker use these same tree and selection contexts, including remaps and disabled keys. Enter opens the selected file's comparison; writable After reuses its live buffer. The catalog contains only changed paths, with A/M/D/R status markers, rather than a disk scan. Ordinary `config.core.file_picker` and `config.core.file_explorer` bindings work inside comparisons. Ctrl+w closes a focused picker/explorer, retaining its query, selection, and expansion state; the panes use the reclaimed width. Ctrl+e has no reserved diff behavior. Escape closes the revision selector in either stage, but an opened comparison still closes explicitly through `:diff.close`, its palette action, or the generated action value below. `config.core.diff` remains the opening action and exposes immutable `close`, `mode.toggle`, `view.toggle`, and `orientation.toggle` children. New comparisons default to Branch changes (merge base → selected After); Normal-mode Ctrl+b, `:diff.mode.toggle`, or the palette toggles to direct Snapshots and back, preserving the selected endpoints. Selecting Working checkout on either side forces Snapshots. `config.core.diff.review.toggle` is a non-mutating Normal-mode action, also available as `:diff.review.toggle` and in the palette. It marks/unmarks the highlighted changed-file leaf when the comparison's explorer is focused, or the current comparison file from a text pane. Read-only snapshots permit review marks. The default `c` shortcut applies only while browsing the diff's changed-file tree; explicit bindings and disabled keys override it. For example, `config.bind "ctrl+g", config.core.diff.review.toggle`; use the palette or a control-key binding while filtering, including List view, so printable input stays in the query. `config.core.pane.close` and `config.core.pane.force_close` close a focused **docked** file list instead of its editor pane, without discarding buffers or exiting the comparison. A floating picker does not intercept these actions: normal close still protects underlying dirty buffers, while force-close retains its existing discard semantics. For example, bind `config.bind "ctrl+x", config.core.pane.close`; focused tree/selection overrides and printable filter input retain priority. Palette-originated close actions retain the originating dock target through force confirmation. Focused selector/filter input receives printable text before Normal-mode action bindings, so `:` stays query text instead of opening Command input. Explicit tree/selection navigation overrides remain effective. Control-key editor action bindings and Ctrl+p remain available. ```ruby config.bind "leader d q", config.core.diff.close ``` Ctrl+h is **not** a Backspace alias or default deletion key in any mode. Raw Ctrl+h (0x08) and Backspace (DEL/0x7f) are distinct, as are CSI-u events. In the tree it navigates in NORMAL and INSERT without deleting query text; it never deletes file-buffer Insert text by default. Explicit `config.core.backspace` bindings remain available if desired. Ctrl+j is distinct from Enter: in the TUI's raw mode, LF is Ctrl+j and CR is Enter; CSI-u control events also work. No enhanced-terminal negotiation is required. Ctrl+m remains an Enter alias. Outside completion and selection lists, Ctrl+k still clones above in Normal/Visual, and Ctrl+j defaults to newline in Insert or submit in Command/Search. While completion is open, Ctrl+j/k cycles candidates; Enter always closes completion and inserts a newline. Enter and Ctrl+j can be rebound independently outside the completion cycle. ## Key sequences and leader Space starts a leader sequence in Normal/Visual when it has no direct binding in that mode. Leader bindings use `leader` followed by whitespace-separated single-key names, for example `config.bind "leader b n", config.core.buffer_next`. Nested prefixes wait for the next key without a timeout. The status bar shows the actual pending sequence before the file path; the palette advertises effective sequences alongside ordinary bindings. Explicit prefixes use the same notation and do not change the leader: ```ruby config.bind "ctrl+w h", config.core.pane.focus_left config.bind "ctrl+w j", config.core.pane.focus_down config.bind "ctrl+w k", config.core.pane.focus_up config.bind "ctrl+w l", config.core.pane.focus_right ``` Press Ctrl+w, release it, then press the direction key. Modified suffixes and deeper nesting also work, for example `"ctrl+w ctrl+j"` or `"ctrl+w p j"`. Explicit prefixes must be unbound in that mode and cannot be Escape, an ASCII digit, or the current leader; use `leader` for symbolic leader routes. The prefix remains reserved while any of its leaves exists, including disabled leaves. Tree/selection navigation overrides retain priority while exploring files; configured control/special-key prefixes can start there, and pending suffixes then belong to the sequence rather than tree navigation. Set `config.leader = ","` to change the trigger for every default and configured leader route. Explicit prefixes do not move. Suffix keys stay unchanged: the default palette binding becomes comma then Space, save becomes comma then `w`, and buffer-next becomes comma then `b` then `n`. Choose an unused single key in the existing notation; Escape, ASCII digits, malformed keys, and a new trigger already bound or reserved as a sequence prefix in Normal/Visual are rejected without replacing the current choice. A direct binding on the current trigger—including `nil`—takes precedence per mode and hides unreachable leader routes from the palette. Esc cancels a pending sequence and count without leaving Visual or collapsing carets. Unknown suffixes consume the key and cancel safely, never falling through to an edit. Paste, external command execution, buffer switching, and binding/leader changes clear pending state. A count before a sequence motion applies normally; digits inside a sequence are literal suffix keys. Plugins execute once. Outside Normal/Visual, sequences have no prefix behavior, so ordinary text still types normally in Insert and prompts. Sequences require Normal/Visual and the target's existing mode eligibility; Ex/plugin targets remain Normal-only. A sequence cannot be both a complete binding and another binding's prefix: ambiguous declarations are refused atomically, including collisions between a single-key binding and an explicit prefix. Disabled leaves remain reserved. Sequences wait without a timeout; timers and macros are not supported. ## Native LSP LSP runs in Rust, independently of Ruby plugin hosts. **Every language server is disabled by default.** Ruby evaluates configuration only at startup; no LSP plugin needs loading. Install a trusted server yourself and explicitly opt in: ```ruby Vamp.configure do |config| config.lsp["rust"] = true # rust-analyzer, extensions: ["rs"] config.lsp["ruby"] = true # solargraph stdio, extensions: ["rb"] config.lsp["python"] = true # ty server + ruff server, extensions: ["py"] config.lsp["my-language"] = { command: ["my-language-server", "--stdio"], extensions: ["myl"], initialization_options: {}, settings: { "my-language-server" => { "exampleOption" => true } } } config.bind "leader d", config.core.lsp.definition config.bind "leader D", config.core.lsp.diagnostics config.bind "K", config.core.lsp.hover config.bind "ctrl+n", config.core.lsp.completion, mode: config.modes.insert end ``` Keys are arbitrary nonempty strings, used as the protocol's `languageId`; they need no Tree-sitter grammar or hardcoded language enum. Absent/`false` entries are disabled. `true` enables a built-in preset: `"rust"` uses `rust-analyzer`/`rs`, `"python"` uses `ty server` plus `ruff server`/`py`, and `"ruby"` uses `solargraph stdio`/`rb`. Unknown keys need a configuration object with nonempty `command` argv or a `servers` list, plus `extensions` (without leading dots). Objects also explicitly enable servers; known presets supply omitted command/extensions. An explicit `command` replaces the entire preset with one server: `config.lsp["rust"] = { command: ["/custom/path/rust-analyzer"] }` retains `rs`, while a Python command override does not also launch Ruff. Only `command`, `extensions`, `initialization_options`, `settings`, and `servers` are accepted. Options/settings must be JSON-compatible objects. Without an explicit command/list, options apply to every default server; explicit `initialization_options` replaces preset defaults. `servers` must be a nonempty ordered list of objects with explicit `command` and optional per-server `initialization_options`/`settings`; it shares language-level extensions and cannot be combined with top-level command/options/settings. Invalid declarations reject configuration with a notice rather than activating a guessed server. Commands are executable/argument arrays, never shell commands; use an absolute path if the executable is not on PATH. No server installation or restart is automatic. Install Solargraph yourself with `gem install solargraph` in the project's Ruby environment. The Ruby preset launches `["solargraph", "stdio"]`, following [Solargraph's stdio integration](https://solargraph.org/guides/language-server), and uses [YARD annotations](https://solargraph.org/guides/yard) for type information. Explicit `command` overrides replace the entire argv: for a bundled installation, use `config.lsp["ruby"] = { command: ["bundle", "exec", "solargraph", "stdio"] }`; Vamp retains the `rb` extension but does not append `stdio` to your override. **Python (Astral):** install [ty](https://docs.astral.sh/ty/editors/) and [Ruff](https://docs.astral.sh/ruff/editors/setup/) yourself, for example with `uv tool install ty` and `uv tool install ruff`. The Python preset sends the same documents to both servers, uses ty for hover/definition, and merges type errors with Ruff lint diagnostics. Clearing or losing one server preserves the other's diagnostics. Formatting and code actions remain unsupported by Vamp. Use `servers` to customize the stack independently; order selects the hover/definition/completion server: ```ruby config.lsp["python"] = { servers: [ { command: ["ty", "server"], initialization_options: { experimental: { useUv: "off" } } }, { command: ["ruff", "server"], initialization_options: { settings: { lint: { extendSelect: ["I"] } } } } ] } ``` **uv remains optional project tooling.** Run project setup/synchronization yourself; [ty discovers an existing `.venv`](https://docs.astral.sh/ty/reference/configuration/#python) without a uv launcher. Vamp neither runs `uv sync` nor changes dependencies/lockfiles. The default ty preset sets `experimental.useUv: "off"` so inherited `TY_UV` cannot enable ty's [experimental environment-management integration](https://docs.astral.sh/ty/reference/editor-settings/). Ruff's initialization options default to `{}`. When replacing ty's initialization options, retain that flag if you want the same policy. Servers start lazily only when a matching buffer opens. Extension matching is case-sensitive; if enabled languages overlap, the first language in sorted key order wins. All matching servers in that language run, and the first handles hover/definition/completion, semantic highlighting, and the diagnostic-status command without fallback to sibling servers. Each server/project root gets one worker, with at most 16 configured servers and 16 server/project keys; Python uses two slots. Start vamp from your project root: its startup working directory is the workspace; automatic root discovery is not implemented. See [native LSP architecture](architecture.md#native-lsp) for synchronization and worker behavior. `:lsp.definition` or the configured Space d binding requests a definition at the primary caret, once regardless of selection count. Both Location and LocationLink results are supported; multiple results use the first. Local-file navigation reuses loaded buffers without overwriting unsaved text. LSP UTF-16 positions and percent-encoded file URIs are translated to validated grapheme coordinates. Navigation is ignored if its source buffer, revision, caret, or latest request has changed. Successful definition jumps center the destination caret's visual row once in its actual text pane, excluding bars and docks. This applies to ordinary buffers, new destination tabs, wrapped text, and split/unified diff panes. Near the beginning of the file, centering clamps to the first line; near EOF, blank space can remain below the centered target. Subsequent cursor motion resumes ordinary scrolling. Failed or stale requests do not request centering; Markdown links, search, and jump-history navigation retain their existing behavior. For programmatic bindings, `config.core.lsp` exposes immutable Normal-mode Action objects `definition`, `diagnostics`, and `hover`, plus the **Insert-only** `completion` Action. Normal actions route to their corresponding Ex commands and have no default keys. Completion defaults to Insert Ctrl+n; override or disable it using an Insert binding. It requests directly without leaving Insert or committing pending typing; there is no Normal `:lsp.completion` command. These actions are also available in the command palette according to mode eligibility. The namespace is distinct from `config.lsp[language_id]` server configuration. Interactive LSP commands use dotted names (`:lsp.hover`, `:lsp.definition`, `:lsp.diagnostics`). The existing whitespace separator keeps the whole dotted name separate from arguments. Current LSP commands take no arguments. Diagnostics appear as severity-colored gutter numbers and text underlines; the primary caret's diagnostic can also appear in the bottom-right notification popup. `:lsp.diagnostics` reports the current document's server state. Older versioned diagnostics are discarded; unversioned diagnostics can only be associated with the arrival-time document revision. Editing clears previous annotations immediately. Server startup/transport failures are reported and clear that server's diagnostics; other servers and native editing remain available. There are no automatic restarts. Insert mode hides all diagnostic gutter colors, underlines, counts, and caret messages. Incoming results are still stored; leaving Insert reveals only the latest results matching the current text revision. Editing invalidates earlier results, so stale annotations never reappear on exit. Document synchronization stays live for completion and semantic highlighting; this display policy does not stop server-side diagnostic analysis. `:lsp.hover` or a configured hover action requests information at the primary caret once, regardless of count or selection count. The response opens a read-only floating popup: arrows/Ctrl+h/j/k/l scroll vertically/horizontally, Home/End select the first/last logical line, and Escape dismisses without collapsing carets. Other input dismisses and continues normal editing. Changes to the source buffer/revision/caret, intervening input, or newer requests prevent pending hover replies from reopening it. Hover accepts plaintext/Markdown MarkupContent and string/code MarkedString forms, including arrays. The popup grows up to 100 columns and 24 rows, clamped to the available document workspace and content height. Multiline content and Markdown markers remain visible, with the same Markdown syntax colors as document buffers; named fenced code blocks use their language's bundled highlighter. Highlighting is cached across scrolling and redraws. Horizontal scrolling exposes long lines without splitting graphemes. Missing/unsupported/invalid hover results report a message without changing text, selections, or undo history. Content is bounded to 4 MiB and terminal controls are sanitized. Completion is manual and uses only the primary insertion caret. Ctrl+n requests it; the first candidate replaces text immediately. While the list is open Ctrl+n/Ctrl+j selects next and Ctrl+k previous, including an Original text row that restores the original document and carets. Other keys close the list with no accept key. Snippets expand the whole template and highlight the first argument: typing replaces it and its mirrors, Tab/Shift+Tab moves next/previous, and the final `$0` ends navigation. Outside a snippet Tab inserts a tab; Escape returns to Normal keeping the displayed text. Secondary carets are translated, not completed independently. Cycling and argument edits belong to the pending Insert undo group; repeat records resolved text and caret placement. Rebind the Insert-only `config.core.snippet.next` / `config.core.snippet.previous` actions normally. See [editing behavior](editing.md#manual-lsp-completion). The first assigned server must advertise a completion provider. Vamp handles item arrays and CompletionList results, plain text, TextEdit, InsertReplaceEdit (using its replacement range), supported edit-range/format/mode defaults, and atomic additionalTextEdits in the same document. Insert-only fallback text replaces the identifier prefix before the caret; `filterText` and stable `sortText`/label ordering are honored. All edit coordinates are validated at UTF-16 and grapheme boundaries against the immutable request document; malformed or overlapping edits report a completion error without retiring a healthy server. Aggregate completion text is bounded to 4 MiB. Pending replies cannot reopen completion after intervening input, source changes, superseding requests, or modal overlays. Supported [LSP snippet syntax](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax) includes numbered tabstops, nested placeholders/defaults, mirrors, escapes, choices (first option), variable defaults, and `TM_SELECTED_TEXT`, `TM_CURRENT_LINE`, `TM_CURRENT_WORD`, `TM_LINE_INDEX`, `TM_LINE_NUMBER`, `TM_FILEPATH`, `TM_FILENAME`, `TM_FILENAME_BASE`, and `TM_DIRECTORY`. Insert has no selected text, so `TM_SELECTED_TEXT` is empty unless a default is supplied. Unsupported regex transforms, unknown variables without defaults, repeated/nonempty final `$0`, or final stops nested inside arguments omit that candidate. Malformed templates report a completion-local error. Each template and expansion is bounded to 256 KiB, with 4096 parsed nodes, 1024 placeholder occurrences, 32 nesting levels, and a bounded rendering traversal. There are no automatic triggers, commit characters, server commands, indentation adjustment, or completion-item resolve requests. Items requiring commands or indentation adjustment are omitted rather than partly applied. Server-specific availability depends on the installed server's advertised capabilities; configuring a preset does not guarantee completion support. Semantic highlighting is **enabled automatically** when the first server advertises [LSP semantic tokens](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens). No additional configuration or binding is needed. Full-document requests are preferred; range-only servers receive a whole-document range request. Token colors override Tree-sitter where recognized; unsupported servers, unknown custom token types, missing tokens, and failed replies retain syntax highlighting. Text/path changes clear old semantic colors immediately, and stale replies cannot restore them. Same-text refreshes keep the current overlay until the replacement arrives. rust-analyzer and ty support semantic tokens. Solargraph 0.61.0 and Ruff do not; Ruby therefore retains Tree-sitter colors with the Solargraph preset, while Python uses ty's tokens and Ruff's diagnostics. Declaration/definition modifiers use bold, deprecated symbols use strikethrough, and readonly variables/parameters/properties use italics without changing their role's color. Diagnostics, search, selections, and carets retain their existing styling precedence. Native LSP currently provides synchronization, diagnostics, go-to-definition, hover, manual completion, and semantic highlighting—not rename, formatting, code actions, or general workspace edits. Semantic token deltas, dynamic registration, and partial-result streaming are not implemented.