33 KiB
Configuration
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.diff_context_lines = 10
config.diff_expand_lines = 15
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.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.
Tabline status label
Set config.tabline_label to a plain string for the right side of the top tab line:
Vamp.configure do |config|
config.tabline_label = ENV.fetch("VAMP_STATUS", "")
end
The default "" disables the label. A nonempty value shows the tab line even with one tab, using the existing accent background and left density fade. The label yields to the active tab when both cannot fit. Unicode is supported; control characters are replaced rather than interpreted as terminal escapes. Non-string values reject configuration.
Configuration runs once at startup. For live values such as battery charge, load a Ruby plugin that supplies updates; the editor does not collect battery data or run periodic shell commands itself.
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. 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+athroughctrl+z,alt+athroughalt+z(option+is an alias foralt+),ctrl+alt+athroughctrl+alt+z(either modifier order),shift+athroughshift+z, andshift+tab.shift+jandJaddress 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 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 is also supported. Cmd/Super, 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.
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 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 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.
Normal dd and cc delete/change complete rows, with leading counts. Bind config.core.delete_line or config.core.change_line to another key or sequence for another route. A configured/disabled d or c, a configured sequence starting with that key, or choosing it as the leader overrides its built-in doubled sequence. These are dedicated line actions, not general operator–motion grammar.
f<char> / F<char> 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 | delete_line, change_line |
| Normal | save, quit, force_quit, save_and_quit, plugins |
| Normal | edit, buffers, buffer, buffer_next, buffer_previous, buffer_delete, buffer_force_delete |
| Normal | follow_link |
| Normal | diff |
| Normal, Insert, Visual, Command, Search | diff.close |
| Normal, Insert, Visual, Command, Search | word_wrap.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 |
| 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. 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.
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 sequence 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 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:
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
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; Working checkout After reuses its editable 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. Close the comparison explicitly with :diff.close, its palette action, or the generated action value below. config.core.diff remains the opening action and also exposes its immutable close child; Escape never closes a comparison.
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:
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:
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, 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.
Python (Astral): install ty and Ruff 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:
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 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. 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 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, 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 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.
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.
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 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. 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.