Files
vamp/docs/configuration.md
T

18 KiB

Configuration

README

Startup configuration, preferences, bindings, and opt-in native language servers. See the example configuration and plugin API.

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.

# @param config [Vamp::Config]
Vamp.configure do |config|
  config.leader = "space"
  config.clipboard_mode = "system"
  config.file_explorer_side = "right"
  config.file_explorer_view = "tree"
  config.file_picker_view = "tree"
  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 "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 leader-prefixed sequence; editing modes are config.modes.normal, config.modes.insert, config.modes.command, config.modes.search, and config.modes.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.

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 and mode strings are not accepted. Registered Ex/plugin commands still use exact string names ("w", "greet", etc., without : or arguments) and can only be bound in Normal mode. 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.

Key notation

Key notation follows Herdr's readable style. 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, and shift+a through shift+z. shift+j and J address the same key.
  • 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 leader sequences. Ctrl+h and Ctrl+j are distinct control keys, not Backspace/Enter aliases. Only a single modifier on an ASCII letter is supported. Arbitrary non-leader key sequences, Alt/Cmd/Super, combined modifiers, modified special keys, and Vim-style angle-bracket notation are rejected rather than silently treated as another key.

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 lsp.definition, lsp.diagnostics, lsp.hover
Normal search, paste_before, paste_after
Normal paste_system_before, paste_system_after
Normal, Visual select, search_next, search_previous
Normal, Visual repeat_last_edit
Normal, Visual file_picker, file_explorer
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

Binding overrides and failures

Adding a binding keeps the original key. Pass nil to disable a key, including literal typing in Insert/Command mode:

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 leader prefixes, or unknown command names leave that binding unchanged and appear in the status message; 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 leader 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, and focused docked explorer share config.modes.selection. It is a binding context, not a buffer editing mode. config.core.down moves to the next list item; 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:

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:

config.bind "ctrl+f", config.core.right, mode: config.modes.tree
config.bind "ctrl+h", nil, mode: config.modes.tree

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 lists, existing editing bindings are unchanged: Ctrl+k still clones above in Normal/Visual, and explicit Ctrl+j defaults insert a newline in Insert or submit Command/Search prompts. Enter and Ctrl+j can now be rebound independently.

Leader key

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", "bnext". 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.

Set config.leader = "," to change the trigger for every default and configured leader route. 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 directly bound 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 leader motion applies normally; digits inside a sequence are literal suffix keys. Plugins execute once. Outside Normal/Visual, the leader has no prefix behavior, so Space still types normally in Insert and prompts.

Leader bindings 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. Disabled leaves remain reserved. Only leader-prefixed sequences are supported—not arbitrary multi-key mappings, timers, or macros.

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:

Vamp.configure do |config|
  config.lsp["rust"] = true # rust-analyzer, extensions: ["rs"]
  config.lsp["ruby"] = true # solargraph stdio, extensions: ["rb"]
  config.lsp["python"] = false # disabled (also the default when absent)

  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
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 pylsp/py, and "ruby" uses solargraph stdio/rb. Unknown keys need a configuration object with nonempty command argv and extensions (without leading dots). Objects also explicitly enable servers; known presets supply omitted command/extensions. For example, config.lsp["rust"] = { command: ["/custom/path/rust-analyzer"] } overrides the executable while retaining rs.

Only command, extensions, initialization_options, and settings are accepted. Both option/settings values must be JSON-compatible objects and default to {}. Invalid declarations reject configuration with a notice rather than activating a guessed server. 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, and uses YARD annotations 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.

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. One worker runs per language configuration/project root, with at most 16 enabled languages/server-project keys. Start vamp from your project root: its startup working directory is the workspace; automatic root discovery is not implemented.

See native LSP architecture 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.

For programmatic bindings, config.core.lsp exposes immutable Normal-mode Action objects: definition, diagnostics, and hover. Each routes to the same native implementation as its Ex command; no LSP keys are bound by default. These actions are also available in the command palette. 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 appears in the message row. :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.

: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. Multiline content is preserved; Markdown is shown as plain text, not rendered markup. 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.

Native LSP currently provides synchronization, diagnostics, go-to-definition, and hover—not completion, rename, formatting, code actions, workspace edits, or semantic highlighting. Tree-sitter remains the syntax highlighter.