Files
vamp/docs/editing.md
T
2026-10-21 12:53:22 +02:00

127 KiB
Raw Blame History

Editing guide

README

Default controls and editing behavior. For custom bindings, see configuration; for language-server commands, see native LSP.

Ordinary documents show repository changes beside their hybrid line numbers by default: green ▌ for additions, red for removals, and yellow for modifications. Configure git_gutter to use + / - / ~, colored numbers, or no change indicators. Bar/symbol gutters keep the order number, blank, marker, blank, text; number-only modes use no extra column. Horizontal scrolling never moves indicators, and wrapped continuation rows do not repeat them.

The baseline is Git HEAD or Git-backed jj's parent @-, so indicators include unsaved edits and remain after saving until the repository baseline changes. A deletion marks the next surviving source row; at EOF it marks the last editor row, including the final empty row after a trailing newline. An empty file anchors removals on row 0. Replacements mark paired rows modified and extra current rows added. Historical snapshots do not show live changes. 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 files, files over 16 MiB, and jj changes with multiple parents have no indicators.

Indicators refresh asynchronously. Typing and undo/redo retain the last completed indicators until the updated comparison arrives, avoiding flashes; indicator values and row positions can briefly lag edits.

Default editing keys

Mode Keys
All modes Ctrl+p opens the command palette; type to filter, Ctrl+j/k or arrows select, Enter accepts, Esc/Ctrl+p cancels
Normal, Visual Space Space opens the palette through the default leader
Normal Space w saves; Space b n/p/l/d switches next/previous, opens the buffer picker, or closes buffers
Normal, Visual Space o opens the floating tree in INSERT/filter mode; Space e opens/focuses the docked compact tree
Normal i insert before each caret; a insert after each caret; o/O open lines below/above
Normal, Visual I (Shift+i) / A (Shift+a) insert at first nonblank / line end; Visual splits each touched row into a caret first
Normal h/j/k/l or arrows move every caret; 0/$ or Home/End
Normal _ splits above/below; | splits left/right; the new pane inherits the current buffer and view
Normal Option/Alt+h/j/k/l focuses the neighboring pane left/down/up/right
Normal Ctrl+Option/Alt+h/l switches to the previous/next tab, wrapping at either end
Normal w/b/e move every caret to next word start, previous/current word start, word end; W/B/E for whitespace-delimited WORDs
Normal, Visual f<char> seeks forward on the current line; F<char> seeks backward; 2fx finds the second x after each caret
Normal gf follows a Markdown file link or heading fragment at the primary caret
Normal, Visual gt goes to the first line; gb goes to the last line, at first nonblank; Visual extends every selection
Normal, Visual <line>g jumps immediately to that one-based source line at first nonblank (42g); clamps at EOF and extends Visual selections
Normal Ctrl+o goes to an older jump location; Ctrl+i goes forward (Tab too outside comparisons); counts select multiple locations
Normal, Visual Decimal prefixes repeat cursor/word/search motions: 5j, 3k, 2w, 4e, 2n; Esc cancels a pending count
Normal, Visual Ctrl+u/Ctrl+d scroll up/down without moving carets or selections; half a viewport by default, configurable with config.scroll_step_lines; counts multiply the distance
Normal J (Shift+j) joins each targeted line with the next line
Normal x/Delete cut the hovered grapheme at every caret; u undo; U (Shift+u) redo
Normal, Visual : enters command mode; Visual selections remain available to selection-aware commands
Normal yy copies whole lines without moving; dd deletes them; cc changes them and enters Insert; counts work (3yy, 2dd, 2cc)
Normal D deletes through line end; C changes that suffix and enters Insert; Y copies that suffix characterwise
Normal, Visual r<char> replaces the current grapheme in Normal or every selected grapheme in Visual
Normal s deletes the current grapheme and enters Insert
Normal d<count>w / d<count>b delete forward/backward word spans, e.g. d2w / d2b
Normal c<count>w / c<count>b cut the same line-local spans as d and enter Insert, e.g. c2w / c2b
Normal, Visual . repeats the last successful native edit against the current targets; one undo group
Normal / opens search; * searches the word under the primary caret; n/N find the next/previous match
Normal v starts characterwise Visual; V starts linewise Visual; p/P paste the configured clipboard after/before each caret or row for linewise fragments (system by default); Esc keeps only the primary caret
Normal, Visual Ctrl+k clones above; Ctrl+e clones below
Visual Cursor/word motions and n/N extend every selection; y copies; d/x/Delete cuts; c changes; v/V switches shape, the same selector or Esc cancels
Visual p / P replaces each selection with the configured clipboard, without overwriting it
Visual Ctrl+l splits touched rows into Normal carets at first nonblank; Ctrl+n adds the next occurrence; Ctrl+s skips the primary occurrence; Ctrl+a selects all occurrences
Insert Type, Enter, Tab, Backspace/Delete, arrows, Home/End at every caret outside snippets; Esc returns to Normal, retaining carets
Insert Ctrl+n requests completion at the primary caret or cycles next; Ctrl+j/k cycles next/previous while completion is open
Insert, active snippet Type to replace the highlighted argument and its mirrors; Tab / Shift+Tab moves next/previous
Command Tab / Shift+Tab completes/cycles command names forward/backward; Enter executes; ↑/↓ or Ctrl+k/j browse history; Backspace edits; Esc cancels
Search Type a literal query; Enter accepts the current occurrence or finds the next match; ↑/↓ or Ctrl+k/j browse history; Backspace edits; Esc cancels
Insert, Command, Search Ctrl+u deletes backward to the logical line start
Insert, Command, Search (macOS) Option+Backspace deletes backward by word; Command+Backspace deletes backward to the logical line start

Outside active snippets, Insert-mode Tab inserts a literal tab by default. Set config.tab_spaces and config.language_tab_spaces to insert a fixed number of spaces, with per-language overrides. The setting applies to every caret and editable diff text; existing tabs and pasted text are not converted.

J removes the next line's leading spaces/tabs and preserves existing trailing whitespace. It normally adds one space, two after ., ?, or ! (Vim's default), and none before ) or when either side is empty. The cursor lands at the join point; the join is one undo action. On the last line, J does nothing.

gt/gb move every caret to the first nonblank grapheme on the first/last row, including the empty final row after a trailing newline. Duplicate carets and overlapping Visual selections normalize as usual. Both Visual shapes retain their anchors and extend to the destination. These motions do not edit text or add undo history; count prefixes are consumed rather than interpreted as line numbers. g waits for its second key, and Escape cancels only the pending prefix. In Insert and command/search prompts, gt/gb remain literal text.

On macOS, these modified Backspace defaults also edit prompt and filter queries, including file lists, the command palette, and revision selectors. Option+Backspace uses Unicode word boundaries, first skipping trailing whitespace; Command+Backspace removes only the prefix before the caret on the current logical line, preserving its suffix and newline. Buffer deletion runs at every caret, merges overlapping ranges, shares the Insert undo group, and dot repeats the boundary operation rather than the old deleted length. Within an active snippet, both operations stay confined to the current argument and update its mirrors. These modified Backspace defaults remain macOS-only; Ctrl+u invokes the same line-deletion action on every platform.

The terminal may deliver a native modifier or a conventional control sequence: Option may use legacy ESC+DEL, and Kitty CSI-u represents Command as Super, not Meta. Ghostty defaults to encoding Command+Backspace as Ctrl+u (\x15), which invokes the same backward line deletion without a Ghostty configuration override. Ctrl+u remains a separate, configurable binding; vamp cannot distinguish it from Command+Backspace when both send the same bytes. Vamp does not negotiate an enhanced keyboard protocol or support generic Command-modified character keys.

Viewport scrolling uses screen rows, including wrapped continuations and diff display rows. It affects the focused text pane (both sides share the diff's vertical offset), and the caret may leave the screen. The next cursor motion or edit resumes caret following. Ctrl+e retains its clone-below binding; single-row scrolling is available through config.core.scroll_up / config.core.scroll_down. Insert, Command, Search, and picker Ctrl+u behavior is unchanged.

Manual LSP completion

With an enabled server that provides completion, Ctrl+n in Insert requests candidates at the primary caret only. The first candidate is inserted immediately and a small list appears near the caret; Ctrl+n or Ctrl+j cycles forward, Ctrl+k backward, with wraparound. There is no acceptance step. The Original text row restores the document and carets from before the result was shown, including any additional edits. Completion never broadcasts a candidate to secondary carets: they are preserved and translated through each chosen edit.

Every candidate is applied from that same immutable original document, not on top of the previous candidate. A candidate's primary replacement and additional text edits are one atomic change. Other keys close the list without accepting it: plain-text completion retains ordinary typing, newline, and configured Tab insertion; snippet completion keeps its argument navigation described below. Escape returns to Normal retaining the displayed text. There is no automatic popup. Empty, unsupported, or invalid results leave the document unchanged and report a message.

Cycling creates no separate undo entries. Only the final chosen edits join the existing Insert undo group; choosing Original text adds no completion edit. Undo restores that group, and dot repeats the final resolved completion edits rather than requests or intermediate candidates. The list stays inside the document text workspace, does not cover the gutter or explorer, and never takes the hardware cursor. Wheel input over it is consumed without choosing a candidate.

Snippet candidates expand the full template immediately, including parentheses and argument defaults. The first numbered argument is highlighted; typing replaces it, then continues within that argument. Repeated occurrences of the same argument update together, without broadcasting to secondary carets. Backspace/Delete stay within the active argument; Enter can insert a newline there. Tab moves forward and Shift+Tab backward in ascending tabstop order. Replacing an enclosing argument removes its nested arguments, not adjacent independent ones. Reaching the final $0 position ends navigation, so subsequent Tab uses the configured spaces-or-tab insertion. Escape, ordinary cursor motion, paste, commands, and focus/buffer changes also end navigation without removing the expanded text.

For example, call(${1:argument}, ${2:count})$0 previews call(argument, count). Type value, Tab, 3, Tab to produce call(value, 3) with the caret after the closing parenthesis. Argument edits remain in the same Insert undo group, and dot repeats the resolved text and final caret rather than re-expanding placeholders.

Numbered tabstops, nested defaults, mirrors, escapes, variable defaults, and context variables are supported. Choices insert their first option, which remains editable. Regex transforms, unknown variables without defaults, repeated/nonempty final $0, and final stops nested inside arguments are omitted rather than inserted partly or literally. See protocol limits.

Word motions

Word motions separate runs of Unicode letters/numbers/underscore from runs of punctuation or emoji. Classification uses each grapheme's first character; combining sequences and ZWJ emoji stay intact. WORD motions treat any non-whitespace run as one WORD.

w/W move to the next start. b/B move to the current start, or the previous start when already at one. e/E move to the current end when before it, otherwise the next end. Motions cross lines but never wrap. Truly empty lines are stops for start motions; whitespace-only lines are skipped. End motions skip empty lines while looking for a word. With no further target, motions stop at the buffer edge, including trailing whitespace or an empty final line.

These keys type literal text in Insert mode. The corresponding actions can also be bound in Insert mode to move all insertion carets without splitting their undo group. Motions never edit text, add history, or clear an active search highlight.

Character seeks

f<char> / F<char> moves every caret to the next / previous matching grapheme on its current line, excluding the current position. A count seeks that occurrence independently for each caret: 2fx finds the second following x, and 2Fx the second preceding x. With too few matches, that caret stays unchanged. Seeks never wrap or cross lines, and Unicode targets land on whole graphemes, including combining sequences and ZWJ emoji.

Normal and both Visual shapes support these motions, including read-only diff panes. Visual keeps its anchors; native edit replay retains the seek's direction, target, and count rather than its original distance. The following character is literal even if it is a digit, leader key, remapped key, or diff expansion bracket. Escape cancels only the pending seek without moving or collapsing selections. f/F remain literal text in Insert and command/search prompts; gf retains its Markdown-link behavior.

Bind config.core.seek_forward / config.core.seek_backward to another Normal or Visual key or leader sequence to start the same target-character prompt.

Motion counts

Counts apply to h/j/k/l, arrows, w/b/e, W/B/E, f<char>/F<char>, and n/N, including remapped keys for those actions. Every caret moves independently before duplicate/overlapping targets normalize. Vertical counts preserve the clamping behavior of repeated steps through short rows; horizontal and word motions stop at buffer boundaries. Character seeks require the requested occurrence on the current line or leave that caret unchanged. Search counts wrap through the available matches.

Unbound ASCII digits 1–9 start a count in Normal/Visual; subsequent digits extend it. 0 retains line-start behavior without a count and extends an existing count when bound to line-start. Explicit digit bindings, including disabled keys, take precedence. Count arithmetic saturates rather than overflowing; large counts stop at motion boundaries or wrap for search, without executing an unbounded repeat loop.

The status bar displays a pending count. Esc cancels it without moving, collapsing carets, or leaving Visual; the next Esc performs its ordinary action. Leader and editing prefixes retain the count until their action resolves. Line/word deletion, character replacement, and substitution use counts; suffix actions and other non-motion commands consume it and execute once. Unknown/disabled keys and bracketed paste clear it; opening the palette clears it, so palette query digits remain literal. Digits also remain literal in Insert and command/search prompts. Counts add no history; only the dedicated operators described below are supported.

Normal editing actions

dd cuts the current line and its newline into the configured linewise clipboard, staying in Normal. cc cuts the same whole-row contents, leaves one empty replacement row, and enters Insert at column 0; indentation is not retained. Leading counts select rows downward from each caret, clamped at EOF: 3dd cuts up to three rows and 2cc changes up to two rows. Overlapping ranges merge before editing or clipboard publication; adjacent ranges remain separate targets. Deleting at EOF removes the preceding separator when needed, and the buffer always retains at least one row.

yy copies the entire current line into the configured linewise clipboard without moving any caret, changing text, or replacing undo/redo and dot-repeat history. Counts before and after y multiply (2yy, y2y, 2y3y), selecting rows downward and clamping at EOF. Empty and unterminated final rows retain linewise shape. p pastes these complete rows below the current line; P pastes above it. The caret lands on the first nonblank of the first inserted row. Characterwise yanks still paste after/before the current grapheme. Bind config.core.copy_line for another Normal-mode route; historical read-only buffers permit yanking but reject paste.

The first d, c, or y waits for its second key and appears in the status bar. d accepts d, w, or b; c accepts c, w, or b; y accepts y. Counts before and after the operator multiply: 2d3w deletes six forward word spans, 2c3b cuts six backward spans and enters Insert, and d2d deletes two rows. Escape cancels only the pending prefix/count, preserving carets; an unmatched key cancels the prefix and then performs its ordinary action. Explicit bindings, disabled first keys, and a leader using that first key take precedence.

System clipboard publication must succeed before any deletion. Failure preserves text, carets, mode, undo/redo, and repeat intent. dd is one undo action; cc and subsequent replacement typing form one Insert undo group. Dot repeats the original requested row count at the current carets—even if the original action was clamped at EOF—and replays cc replacement text. Rebind the Normal-only config.core.delete_line / config.core.change_line actions to a single key or leader sequence without relying on the built-in doubled keys.

D cuts from each caret through its logical line end; C cuts the same suffix and enters Insert. Y copies that suffix characterwise without moving the carets. These actions leave line separators intact and ignore counts. C and subsequent typed replacement form one undo group.

r<char> replaces one grapheme per caret and stays in Normal; s cuts one grapheme and enters Insert. Counts select that many graphemes, clamped to the current line. Combining sequences and ZWJ emoji are never split. Replacement accepts a printable character or Tab literally; Escape or other non-character input cancels it without editing. Empty-line replacement is a no-op; substitution can still enter Insert. s and replacement typing form one undo group.

dw / db cut characterwise spans from each caret to the forward/backward word-start boundary, using the same Unicode word boundaries as w / b, but confined to that caret's current logical line. Counts repeat the boundary motion, stopping at the line end/start without deleting its newline or touching neighboring lines; ordinary w / b navigation still crosses lines. Backward spans exclude the original caret character. Empty spans do not edit text. These are dedicated actions, not arbitrary operator–motion grammar. Suffix, substitution, and word cuts share the configured clipboard's failure-before-edit protection; all native edits support undo and dot repeat.

cw / cb cut exactly the same line-local spans as dw / db, including intervening whitespace, then enter Insert at each cut's start. Counts before and after c multiply (2cw, c2w, 2c3b); excess counts stop at the current line's boundary. Empty spans still enter Insert, including on empty lines and for cb at column zero. The cut and subsequent replacement typing form one undo group; dot repeat recomputes the requested spans at the current carets and replays the replacement text. Rebind the Normal-only config.core.change_word_forward / config.core.change_word_backward actions for another route.

Selections and multi-caret editing

Editing uses an ordered selection set with one primary target. Cursor and word motions apply to every target; Visual motions keep each target's anchor and move its active endpoint. Whole combining sequences and ZWJ emoji remain intact. v selects characterwise, inclusive of endpoints; V selects every complete row touched by the endpoints, including blank and empty final rows. Switching between v and V retains endpoints; pressing the current shape's selector again cancels. Read-only ranges are end-exclusive. Characterwise $/End can select the newline after a non-final row; selected newlines appear as a blue blank cell after the line. Linewise highlighting covers the copied text, including trailing whitespace, plus one blank cell for each copied newline; unused viewport space and the gutter remain unhighlighted. The status reads VISUAL LINE. Blue highlighting overrides yellow search highlighting; secondary carets are magenta, including on empty lines and at line ends. Only the primary owns the hardware cursor and viewport. The status line counts all targets, including offscreen targets.

Ctrl+k/Ctrl+e clone each target to the adjacent row, preserving preferred terminal-cell columns (tabs use four-cell stops); Visual cloning preserves both endpoints. Duplicate carets and truly overlapping selections merge; adjacent ranges remain distinct. Visual Ctrl+l creates one Normal caret at the first nonblank of each touched row. Visual I/A similarly create one Insert caret per touched row at first nonblank/end. Occurrence actions use the primary selection as a literal, potentially multiline, grapheme-aligned query: Ctrl+n adds the next non-overlapping occurrence and wraps; Ctrl+s moves only the primary to that occurrence; Ctrl+a selects all occurrences.

Copy and cut return to Normal at each selection start; Visual cancellation keeps each active endpoint and removes highlighting. Normal Esc collapses to the primary. Characterwise p/P paste after/before each current grapheme, including multiline text. Linewise clipboard fragments paste complete rows below/above each target row, including at end of file. Internal fragments retain their shape; system clipboard shape is retained only while its bytes match the editor's last publication, and unrelated external clipboard text is characterwise. Characterwise paste lands on its last inserted grapheme, or the new row's start when its fragment ends in a newline; linewise paste lands at the first nonblank of the first inserted row. Every multi-target edit is planned against the original buffer and committed as one undo action, not one per target. Copy, movement, and cancellation do not add history or clear redo. Undo/redo restore text, the full caret set, and primary identity in Normal mode, not Visual highlighting or the clipboard.

In Visual and Visual Line, both p and P replace each complete selected range rather than inserting beside its caret. Clipboard fragments are inserted literally, including their newlines: a characterwise fragment replacing a whole-line selection does not gain a newline, and a linewise fragment replacing characters keeps its newline. The edit returns to Normal, is one undo group across all selections, and supports dot repeat with the captured clipboard and selection intent. Neither clipboard is overwritten by the replaced text. Failed clipboard reads, invalid text, empty clipboard, or mismatched fragment counts leave the selection and document intact.

Visual c removes all selected text into the configured clipboard and enters Insert at each selection start. In the default System policy, clipboard publication must succeed before any deletion; failures leave text and selections intact. Deletion and replacement typing form one undo action when Insert mode is exited, including through explicit command execution. Esc without typing commits the deletion alone. An identical replacement creates no history entry and preserves redo. Changing only characterwise empty final-row selections enters Insert without overwriting either clipboard; linewise empty final rows are genuine rows and publish a newline.

Visual r<char> replaces every selected grapheme with the literal character, preserving newline separators and the clipboard. It returns to Normal at each selection start as one undo action and supports . repeat. Esc while waiting for the character cancels the replacement without clearing selections. Identical replacements and newline-only selections do not add history or discard redo.

Bracketed paste broadcasts the same literal text to all Insert carets in a separate undo group, distinct from typing before/after it. A rejected paste leaves the pending change intact. Normal supports counted yy, dd, cc, dw, db, cw, and cb, but not arbitrary operator–motion grammar; blockwise Visual is also unsupported. c, d, and y type literal text in Insert mode.

Clipboard policy is session-wide and defaults to the system clipboard. Copy/cut store fragments in document order: one fragment broadcasts to every caret; matching fragment/caret counts pair in document order; any other count refuses paste without changing editing state. Empty clipboard paste does not edit text; copying/cutting only characterwise empty final-row selections does not overwrite either register. Linewise copy/cut retain whole-row shape, including a newline fragment for an empty final row. Open a new search after leaving Visual mode; Ex commands can be entered directly with : while retaining the selection. Plugin commands execute once, not once per target: successful text effects collapse to the primary and apply once; rejected or message-only effects preserve the selection set. There is no selection-aware plugin API yet.

Clipboard policy

Normal x/Delete (config.core.delete) copies each hovered grapheme to the configured clipboard before removing it. Combining sequences and emoji stay intact; empty lines are unchanged, and an entirely empty cut preserves the clipboard. Dot-repeat cuts and copies the current graphemes. Insert-mode Delete and its replay do not copy.

config.clipboard_mode = "system" is the default: ordinary Visual y/d/x/c and Normal/Visual p/P use the OS clipboard. External clipboard changes are read on each paste. Set "separate" to keep ordinary operations on vamp's internal fragment register instead. Both registers remain independent across buffers; changing the policy does not overwrite either.

Explicit config.core.copy_system (Visual) and config.core.paste_system_before / paste_system_after (Normal/Visual) always use the system clipboard in either policy. They appear in the command palette and can be bound without replacing ordinary internal operations:

config.clipboard_mode = "separate"
config.bind "ctrl+s", config.core.copy_system, mode: config.modes.visual
config.bind "ctrl+g", config.core.paste_system_before

For other applications, characterwise multi-selection exports join fragments with LF; linewise exports concatenate whole-row fragments without extra separators because each fragment already ends in LF. Vamp retains their original boundaries and shape privately while the OS text exactly matches its export, so its own copies still paste pairwise. Different external text invalidates that metadata and becomes one characterwise literal fragment—even when it contains newlines—broadcasting to all targets. Identical external replacement text is inherently indistinguishable from an unchanged export. No empty-system fallback to stale internal text occurs.

System paste validates UTF-8, unsupported controls, newlines, and a 4 MiB limit before editing; CRLF/CR normalize to LF. System exports also have a 4 MiB limit. Provider failures report an error without deleting/changing buffer text, caret/selection state, history, or repeat intent. No silent internal fallback occurs in System mode; choose Separate if clipboard access is unavailable. A native provider that fails after publishing may already have changed OS clipboard formats; vamp cannot roll that external side effect back.

macOS copies publish UTF-8 plain text through the native pasteboard API, and pastes read text directly through AppKit, avoiding interpreter/process startup for every copy, cut, or paste. Linux Wayland uses wl-paste/wl-copy, and Linux X11 uses xclip. Linux tools must be on PATH with the corresponding display environment. Other platforms report unsupported access; choose Separate mode when system clipboard access is unavailable.

Repeat last edit

. repeats the session's last successful native edit in Normal/Visual. Insertion placements (i/I/a/A/o/O), Insert typing and edit-context motions/newline/backspace/delete, Visual change/cut, counted Normal dd/cc, Normal character deletion, clipboard/literal paste, and line joining are recorded as resolved edit operations—not keystrokes, command invocations, or old byte offsets. Remapping keys afterward does not change the recorded operation. Insert . and popup query . remain literal. A count before dot is consumed and repeats once; a stored dd/cc recipe retains its own requested row count.

In Normal, characterwise Visual change/cut targeting replays its resolved motions and counts at each current caret: vecitem followed later by . changes a fresh whole word, even if its length differs. If characterwise targeting was rebuilt by cloning, occurrence gathering, search, or overlap merging, repeat uses relative whole-grapheme spans instead. Visual linewise targeting uses signed relative row spans, preserving whole-row shape rather than replaying motions. Equal spans broadcast; unequal spans require matching target counts. In Visual, current explicit ranges and selection shape replace stored Visual targeting. Native dd/cc recipes instead always select their requested rows from each current active caret. Target-building commands themselves do not recreate old cursors.

Clipboard paste captures the fragments and their characterwise/linewise shape actually pasted, so a later yank or external clipboard change does not change repeat's payload; dot paste never rereads the OS. One fragment broadcasts; equal fragment/target counts pair; mismatches refuse atomically. Other per-target edit data, such as partially effective Insert deletion, obeys the same conservative distribution policy. Dot cut/change follows the current clipboard policy and stages publication until replay validation finishes. A refusal or provider error preserves editor text, caret/primary state, register caches, history, and recipe; already-published OS data cannot be transactionally restored.

Each repeat returns to Normal and creates one undo entry restoring the full caret set and primary. Original typing/paste/typing boundaries remain separate; dot replays the selected committed group as one transaction. Typed text retains scalar insertion boundaries, preserving combining/ZWJ interactions with surrounding graphemes; bracketed paste remains one bulk literal operation. Trailing Insert movement is not repeated.

The last recipe is session-wide across buffers, but pending Insert groups and undo/redo remain buffer-local. No-op/identical edits, rejected input, navigation, yank, explorer filtering, saving, undo/redo, and plugin effects do not replace it. Successful buffer parking and ordinary explicit Ex execution commit pending native typing at existing boundaries; failed buffer requests do not split that group. Plugins never execute through dot. Repeat does not consume or recursively replace its recipe. Rebind with config.core.repeat_last_edit, including Normal/Visual leader routes.

Search is case-sensitive and single-line, with no regular expressions or Unicode normalization. Matches start and end on whole grapheme boundaries, including combining marks and emoji. Next/previous navigation skips the current position and wraps through the buffer; a sole match wraps back to itself. All occurrences of the active query are highlighted yellow.

Submitting / keeps each caret in place if it is at or inside an occurrence of the query; otherwise, it moves that caret to the next match, wrapping as needed. This also applies to an empty submission repeating the previous query. Use n/N to explicitly move to the next/previous match.

Normal * highlights the primary caret's word when it selects a new query or changes the matching mode. Carets at or inside a matching occurrence stay put; carets outside one move to the next occurrence. Pressing * again for the same active word navigates forward; counts such as 2* then skip to the requested occurrence, with every caret searching the primary word from its own position. Selecting a different word activates its search using the same positioning rules. It uses the same Unicode letters/digits/underscore boundaries as w, so cat skips scatter, cat_cat, and cat2. If the caret is not on a keyword, it takes the next keyword on that line; with none, it uses the punctuation run under or after the caret as a literal substring. A blank remainder reports no word and preserves the previous search. This also works in read-only diff text. Bind config.core.search_word to remap it.

n/N and an empty / submission retain the whole-word setting; a nonempty / query returns to literal substring search. Cancelling / preserves both the previous query and its matching behavior.

Normal-mode Escape hides search highlights without forgetting the query or matching mode. The next * reactivates highlighting, staying put only when at or inside a match; n/N or submitting / resumes navigation and restores highlighting. Escape used to cancel a prompt, selection, or pending motion keeps its existing behavior.

Submitting an empty search repeats the last query. Esc cancels without moving the cursor or replacing the previous query/match. A missing match reports the query and leaves the cursor unchanged; searching without a previous query also leaves the cursor unchanged. Search never edits text or adds undo history. Buffer edits and undo/redo retain the query and recompute its visible highlights, so subsequent navigation searches the current text again.

Commands, saving, and pasted input

: opens compact floating command input titled Command; / opens input titled Search for buffer text search. Titles sit at the top-left border, replacing the displayed leading : / /. Both input fields have one column of padding on each side. Enter submits and Escape cancels using the existing modal rules. Long Unicode input scrolls horizontally without changing buffer text. Both windows default to the workspace's bottom-left edge, directly above the bottom bar when visible; their anchors can be configured independently. They temporarily hide operation notifications and never reserve a permanent prompt/footer row. Tiny windows omit borders/titles or padding when needed to keep input usable.

Up/Ctrl+k recalls older entries; Down/Ctrl+j recalls newer entries and restores the unfinished input after the newest. Navigation stops at either end without executing a command or moving search targets. Recalled text is editable without changing stored entries. Command and search histories are separate, shared across buffers/panes/tabs, and retained only for this editor session, up to 100 entries each. Enter records nonempty submissions, including unknown commands and searches with no matches; consecutive duplicates are skipped. Empty submissions and cancelled drafts are not recorded. Ctrl+j no longer submits Command/Search input; use Enter.

Tab replaces the command-name prefix with the first case-sensitive match in alphabetical order; repeated presses cycle forward through the same matches and wrap. Shift+Tab cycles backward, starting at the last match when no cycle is active. Switching direction retains the original prefix. Candidates include canonical core commands, shorthand aliases, and commands from currently enabled plugins. Empty input cycles all command names. Typing, deleting, or browsing history starts a fresh completion cycle. Completion only changes the prompt: Enter still executes, and Escape cancels. Arguments (including paths) are not completed; Tab and Shift+Tab leave them unchanged. Search-mode Tab remains literal text. Rebind or disable forward/backward completion with config.core.command_complete / config.core.command_complete_previous in Command mode.

Command input works in both Normal and Visual modes, including read-only diff panes. The original selection stays highlighted while typing. Enter dispatches with its ranges, direction, and linewise shape intact; individual commands decide whether to consume or retain them. Escape returns to the original Visual selection without executing. For example, select lines with V, then type :diff.comment and press Enter—no command palette is needed. config.core.command can also be bound in Visual mode.

Use :!COMMAND (for example :!pwd or :!echo "$PATH") to run shell commands asynchronously while continuing to edit. Vamp runs $SHELL -c COMMAND, falling back to /bin/sh, in the editor's project directory with the inherited environment and closed stdin. Shell quoting, pipes, and redirection work normally; interactive terminal programs are not supported. Commands have the same permissions as Vamp, including file and network access. Submit additional :! commands while others are running to execute them concurrently.

While running, the default top bar uses one independently styled segment immediately left of the battery for all pending commands. Each entry shows the first three command graphemes, ... when truncated, and an animated spinner after a 200 ms delay. Completion opens a read-only floating window with the command, exit status (or launch error), stdout, and separately marked stderr. Further completed results queue without replacing the visible window; Enter/Escape dismisses the current result and opens the next, or resumes the underlying editing mode when none remain. Arrows/Ctrl+h/j/k/l and the mouse wheel scroll; Home resets and End goes to the last line. Output is decoded as UTF-8 with replacements; controls are displayed as text, never interpreted as terminal escapes. Capture retains up to 4 MiB per stream with an explicit truncation notice. There is no command timeout; exiting Vamp kills all running command process groups, and background jobs in each group are also stopped when their command exits. Shell execution currently requires Unix.

In the shell output window, your configured config.core.word_wrap.toggle binding toggles soft wrapping, including leader and multi-key sequences. Bindings use Normal mode while the popup is focused; there is no additional default shortcut. This does not change output bytes or the source buffer's wrap setting. Wrapping starts off for each command; toggling resets scrolling to the start. With wrapping on, vertical scrolling follows visual rows and horizontal scrolling is disabled.

The latest reported operation result appears in a non-focusable popup at the workspace's bottom-right, just above the bottom bar when visible, with one column of inner padding on each side when space permits. It remains until dismissed or replaced, with no timer. In Normal mode, the first Escape dismisses it without collapsing carets; a subsequent Escape retains ordinary Normal behavior. Existing modal windows, focused explorers/diff selectors, and pending prefixes retain Escape priority. New operation messages reopen the popup; routine successful buffer switches do not duplicate the buffer/path information already shown in the bar.

Command-capable core actions derive canonical Ex names by removing core.: core.quit gives :quit, core.save gives :save, and core.diff.close gives :diff.close. Native editing and Insert-only completion/snippet actions remain action bindings, not additional Ex commands.

Commands: :save [path], :force_save [path], :quit, :force_quit, :restart, :force_restart, :save_and_quit [path], :force_save_and_quit [path], :plugins, and :plugin-command [arguments]. Short names remain aliases of these canonical commands, with identical argument and safety rules:

Alias Canonical command
:w :save
:w! :force_save
:q :quit
:q! :force_quit
:restart! :force_restart
:wq :save_and_quit
:wq! :force_save_and_quit
:bnext :buffer_next
:bprev :buffer_previous
:bdelete :buffer_delete
:bdelete! :buffer_force_delete

:q refuses unsaved changes in any open buffer; :q! exits without saving them. :wq saves the active buffer and quits only if every buffer is then clean; :wq! applies the same quit rule after a force save. The typed actions are core.save, core.force_save, core.save_and_quit, and core.force_save_and_quit. Normal saves re-read actual disk content and compare it with the saved baseline after CRLF/CR-to-LF normalization, independently of polling or timestamps. External changes or deletion refuse the write with a warning; a new buffer opened at a nonexistent path may save normally. Use :w! / :force_save [path] or :wq! / :force_save_and_quit [path] to explicitly overwrite external changes. Force bypasses only the external-change check: historical snapshots remain read-only, target symlinks are rejected, and another loaded buffer's path cannot be overwritten.

:restart closes clean buffers across all tabs, panes, and historical views. If any dirty buffer remains, it preserves that buffer's unsaved text and undo/redo history, warns, and keeps the editor interactive; clean buffers already closed stay closed. With no dirty buffers, it restarts the executable. :force_restart (alias :restart!) restarts regardless of dirty buffers, discarding unsaved text without writing it. Both commands take no arguments; their Normal-mode actions are core.restart and core.force_restart, with no default keybindings. Selecting force restart in the command palette requires confirmation; an explicitly configured force-restart binding executes directly.

Restart tears down the entire editor session, including workers and plugins, before relaunching the same executable with its original CLI arguments. It briefly resets raw mode and input reporting but retains the alternate screen across the process handoff; the new process resumes input modes and clears and repaints that screen atomically in its first frame. The new process rereads configuration and reopens the original startup file from disk, or starts with an unnamed buffer if no file was supplied. Current buffers, tabs, views, and other session state are not restored. Final quit, errors, and panics still restore normal input modes, the normal screen, cursor visibility, and the prior panic hook.

All saves use a same-directory temporary file and rename and preserve existing permissions. UTF-8 text is edited by extended grapheme; tabs render at four-cell stops. LF and trailing-newline structure are preserved; CRLF and CR are normalized to LF. Binary/terminal-control-containing text is rejected. Bracketed paste inserts only in Insert mode and never executes pasted commands in Normal mode.

Live file buffers automatically check disk metadata every 250 ms, including buffers in inactive tabs/panes and parked views; historical snapshots are excluded. Content is read only when metadata changes. A clean buffer reloads validated changed text, clears stale undo/redo, preserves caret/selection and view positions where possible, and clamps them to the new text. Reloads invalidate annotations and stale asynchronous requests and refresh live diffs. Dirty buffers retain their edits. Missing, unreadable, or invalid changed files preserve buffer text and report a warning rather than replacing it.

Paste is backend input: it is ignored in Normal, Command, Search, and Visual modes, and inserts literal text in Insert mode, with CRLF/CR normalization and a 4 MiB limit. Pasted text is one undo action, separate from typing before and after it. Rejected paste reports an error without changing text, cursor, mode, or pending insert history. Internal clipboard actions are separate from Input::Paste and use already-validated buffer text.

Cursor-following scrolling

Configure scroll_distance to reserve space between the primary cursor or picker selection and the viewport edges. 0 (default) scrolls only to keep it visible; an integer reserves that many visual rows, and columns for unwrapped document text. nil, or a margin too large for the current viewport, centers it as far as the start of content permits. This policy applies to documents, historical/diff panes, file explorers, palettes, completion lists, and revision pickers. Reversing direction inside the margin does not move the viewport; resizing recalculates the usable margin.

Mouse wheel scrolling

Mouse-wheel up/down scrolls the document view three display rows per event without moving the primary or secondary carets, changing selections, or editing text. The caret may scroll offscreen; its terminal cursor is hidden until visible again. Navigation (including a motion blocked at the document boundary), editing, or buffer navigation restores caret-following. Idle updates preserve a wheel-scrolled view, clamped to the document; resizing preserves unwrapped views and restores caret-following in ordinary wrapped buffers.

Mouse-wheel left/right scrolls three display columns per event when wrapping is off. Shift+wheel up/down provides the same left/right scrolling when the terminal does not send horizontal wheel events. Tabs and wide Unicode characters use terminal-cell widths; scrolling stops at the longest line and remains detached from the caret until navigation or editing resumes. Wrapped text consumes horizontal wheel input without panning.

Vertical wheel input over a hover popup or floating/docked file explorer scrolls that panel instead. Explorer scrolling preserves the selected file, filter, and keyboard focus; keyboard input resumes selection-following. An offscreen selected entry has no browsing cursor, while the filter input keeps its own cursor. Horizontal wheel input over these panels is consumed without panning the document. Completion lists, command palettes, and startup notices consume wheel input without scrolling the background or editing text. Mouse clicks and dragging are not editing actions.

Word wrap

Use :word_wrap.toggle, the Toggle word wrap palette action, or config.core.word_wrap.toggle in a custom binding. Wrapping defaults to off for new source buffers and on for local review buffers, with no default keybinding. Each ordinary pane retains its own setting for each buffer; toggling does not edit text or enter undo history.

Toggling preserves the source caret and anchors a visible caret to its previous screen row in ordinary and diff buffers. File boundaries clamp the anchor when there are not enough preceding rows. Horizontal position follows the reflowed text.

Text wraps at word boundaries where possible, with grapheme-safe breaks for long words. Word-break opportunities require non-whitespace text on the visual row, so indentation alone does not push a long identifier onto a new row. Whitespace is preserved; tabs keep their source-column stops and wide/combining Unicode characters retain their geometry. Only the first display segment shows a line number. Syntax and semantic colors, diagnostics, search matches, selections, and cursors follow the wrapped text. Motions and counts still use source lines, so j/k, Home/End, and saved file contents are unchanged. Vertical wheel input scrolls continuation rows; switching wrapping off restores horizontal scrolling.

Current-line Git blame

Inline blame is enabled by default in live file buffers and on the focused side of split/unified diffs. The primary caret's line shows its author, commit summary, and short commit ID after the document text; changed or newly inserted tracked-file lines show Uncommitted, including unsaved edits. Live Git-backed jj buffers use the working-copy revision's single parent, not a potentially stale Git HEAD. Merge working copies with multiple parents omit live attribution.

Loading happens in the background without network access or changing repository refs/checkouts. Attribution is loaded once for the active buffer's in-memory contents; navigation then selects cached lines immediately, and movement during a cold load does not restart it. Editing, saving, reloading, changing source buffers, regaining terminal focus, or toggling blame invalidates the cached attribution. Historical sides use their captured commit and side-specific path, including renamed or deleted files no longer on disk; historical tabs retain that source and any privately fetched objects after the comparison closes. Frozen Working sides use their captured parent baseline rather than a later checkout. Missing, untracked, unborn, and non-repository live files have no indicator. Long attribution labels clip to the spare cells after the source text instead of disappearing entirely. The indicator is hidden when the caret is offscreen or no inline space remains; wrapped lines use their final visible continuation without covering text, selections, or the caret. Popups retain their normal layering.

Use :git_blame.toggle, the palette action, or remappable config.core.git_blame.toggle; there is no default keybinding. Set config.git_blame = false to disable it at startup. Disabling stops the worker and clears the label without changing text or undo history.

Panes and splits

Splits can be nested in either direction. The new pane receives focus. Panes showing the same buffer share text, saved state, diagnostics, and undo/redo history, but retain independent carets, selections, modes, searches, and vertical/horizontal viewports. Buffer navigation changes only the focused pane. A file is still loaded once, even when displayed in several panes.

Canonical command Typed action Behavior
:pane.split_horizontal config.core.pane.split_horizontal Split above/below (_ in Normal)
:pane.split_vertical config.core.pane.split_vertical Split left/right (| in Normal)
:pane.focus_left / :pane.focus_down / :pane.focus_up / :pane.focus_right Corresponding config.core.pane.focus_* action Directional focus (Option/Alt+h/j/k/l in Normal)
:pane.close config.core.pane.close Close a focused docked file list, otherwise close the focused editor pane subject to unsaved-change protection
:pane.force_close config.core.pane.force_close Close a focused docked list without discarding data; otherwise explicitly discard exclusively displayed edits and close the editor pane

Closing a pane gives its space to its sibling:

Situation Close pane Force close pane
Focused docked explorer or diff file list Close only the list and return document focus; retain buffers, edits, tabs, and comparison Same
Clean buffer displayed only here Close pane and buffer Same
Dirty buffer displayed only here Refuse; save or force-close explicitly Discard edits and close pane and buffer
Buffer also displayed in another pane or tab Close only this pane; retain the buffer and edits Same
Last pane in a tab, with other tabs open Close the tab using the same shared-buffer and unsaved-change rules Same, discarding only exclusively displayed edits
Last pane in the last tab Quit only if every open buffer is clean, including hidden buffers Quit without saving

There is no default close-pane binding. Option must send Alt/Meta in the terminal; on macOS, configure the terminal's Option-key behavior if it instead produces special characters. These bindings do not replace existing Ctrl+j or uppercase motion/edit bindings.

A focused docked list is a close-action target even when it is the only visible side panel; closing it never quits or closes its buffer. A visible but unfocused dock does not intercept pane-close. The command palette remembers a dock that had focus when it opened for these two close actions, including force-close confirmation and whitespace edits to that command; cancelling or choosing another action clears that target.

Floating pickers are not pane-close targets. A resolved close/force-close action passes through to the underlying editor pane/tab with its existing dirty-buffer guards; it is not ignored and does not merely dismiss the picker. A surviving tab-owned floating picker remains open until its ordinary Escape/close behavior or owner closure removes it. Printable bindings remain query text while a filter owns input; control-key or control-prefix bindings can invoke pane-close without altering the query.

Pane-focus bindings also work while the ordinary file explorer or picker has focus. Moving to a neighboring pane returns keyboard focus to its document; a floating picker closes, while a docked explorer remains visible. Explicit tree or selection navigation overrides and disabled keys take precedence over pane-focus bindings.

Floating windows overlay the entire editor, above all workspace panes. File pickers, revision selectors, hover documentation, and completion lists are not clipped to the focused pane; completion remains anchored to its caret. Docked exploration reserves space at the editor edge before the remaining workspace is split into panes. The changed-file explorer follows the same layout, on the editor's right edge.

Diff comparisons reuse the split geometry and directional-focus engine. Option/Alt navigation moves between Before, After, and Files first, then to a neighboring workspace pane when there is no internal target in that direction. Splitting a diff text view creates an ordinary sibling showing the same buffer; historical snapshots remain read-only, including after the comparison closes.

Tabs

Use :tab.new to open the current buffer in a new, focused tab with one pane. Each tab preserves its split layout, focused pane, explorer, and comparison. Buffers and undo/redo history remain shared; carets, selections, searches, wrapping, and scroll positions are independent views. Opening a historical diff buffer in a tab keeps it read-only, even after the source comparison closes.

Canonical command Typed action Behavior
:tab.new config.core.tab.new Open the current buffer in a new tab
:tab.previous / :tab.next config.core.tab.previous / config.core.tab.next Switch tabs, wrapping at either end; Ctrl+Option/Alt+h/l in Normal
:tab.close config.core.tab.close Close the current tab and all its panes, protecting unsaved changes
:tab.force_close config.core.tab.force_close Explicitly discard exclusively displayed edits and close the tab

By default, multiple tabs show a single top row with numbered filenames and dirty markers, with padding inside each tab's styling. The active tab has an accent-colored background fading into the terminal background through ░▓ / ▓░, except the first tab has no left fade; inactive tabs remain unfilled. Narrow terminals keep the active title visible. New/close actions have no default keybinding; for example, config.bind "leader t", config.core.tab.new.

The top and bottom bars accept the same builtin and custom segments, anchored left, truly centered on the full row, or anchored right. Either bar can show tabs or status information, with independent title templates, internal padding, and styled edge glyphs. Defaults keep tabs and battery above, with home-abbreviated paths on the bottom left and language/ready LSP names/position on the bottom right. Floating input and operation notifications overlay the workspace without reserving a footer. Empty content consumes no row, and a bar whose only visible content is tabs is hidden with a single tab in either position. Custom named values are session-wide and can update through plugins without editing buffers; empty values hide their segments. Both bars share collision handling: 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.

At very small heights, both bars yield to a document row; floating input still accepts text on a one-row terminal. If only one bar row fits while both have content, the top bar receives it.

Closing a tab refuses dirty buffers whose final displayed owner would disappear, including a comparison's working and return buffers. Buffers displayed in another tab remain open with their edits, even on force-close. Other hidden buffers remain loaded. Closing the last tab quits only if every open buffer is clean; force-close quits without saving.

Buffers

Each buffer retains its shared text, path, saved state, and undo/redo history. View state is retained separately for each pane and buffer. Caret-following adjusts that view's offsets as needed; mouse-wheel scrolling can temporarily detach it from the caret. Clipboard fragments, bindings, plugin hosts, and plugin retirement remain session-wide. Plugin context and effects refer to the focused pane's active buffer.

Command Behavior
:edit PATH Switch to an already-loaded file, or read and validate a new buffer; a nonexistent file opens empty
:buffers Open the searchable buffer picker; stable IDs and paths, * before the active buffer ID, + dirty
:buffer N Switch to buffer ID N
:buffer_next / :buffer_previous Switch in ascending-ID order, wrapping at either end
:buffer_delete [N] Close the active or named buffer; refuse unsaved changes
:buffer_force_delete [N] Explicitly discard changes and close the active or named buffer

IDs start at 1 and are never renumbered or reused. Closing the last buffer creates a fresh unnamed clean buffer rather than quitting. Closing the active buffer selects the next remaining ID, with wraparound. Switching does not save or discard hidden edits; automatic disk checks also cover hidden clean buffers. Existing-file aliases resolve to the same loaded buffer; the supplied path spelling remains the save/display path, so opening through a symlink does not bypass save rejection. Paths are relative to the process working directory; the entire :edit argument is the path, including spaces, with no shell expansion.

Switching from Insert commits its pending undo group and parks that view in Normal. Explicit buffer switching from Visual retains the pane's directed ranges and Visual mode for that buffer; switching back restores them. Command/search prompts, palette queries, and pending motion counts are transient, not restored per buffer. Buffer commands appear in the command palette and can be bound by their typed core actions in Normal mode. When several buffers are open, status shows the active ID and total count.

Space b l, :buffers, or the Buffers command-palette action opens the picker. Type to filter IDs and paths with case-insensitive, whitespace-separated substring tokens; every token must match. Results retain ascending ID order. Ctrl+j/Ctrl+n/Down and Ctrl+k/Up select with wraparound, using the shared Selection bindings and overrides. Backspace edits the query; Ctrl+u clears it. Enter switches only the focused pane to the selected buffer without saving or discarding edits; no matches leave the picker open. Esc or Ctrl+p cancels without switching. Printable keys, including : and leader keys, belong to the query.

The picker uses the file explorer's centered floating-panel geometry, a cyan border, padded search input, and dimmed controls. Buffer IDs and status columns align across rows: IDs use the normal text foreground, a cyan * before the ID marks the active buffer, and a two-column gap separates the ID from the filename. Unsaved buffers show a yellow + in the first gap column; the selected row is blue. Paths under the home directory display as ~/…, using the same component-aware abbreviation as the status bar; searching and file operations retain the original paths. Narrow or short terminals drop padding/borders as needed while keeping the query cursor and selected result visible whenever a result row fits.

The picker includes ordinary buffers from every tab and unnamed buffers as [No Name]; immutable diff snapshots are excluded. Labels sanitize path control characters, but selection uses stable buffer IDs, so displayed-name collisions still select the correct buffer. Switching out of a diff follows ordinary :buffer N behavior.

File picker and explorer

Space o opens a floating Tree explorer directly in INSERT/filter mode, ready for typing; Space e opens or focuses the docked Tree, starting in NORMAL/navigation mode when new. Both use one catalog and instance. Invoking the other action applies that presentation's configured default while retaining query, selection identities, and surviving folder expansion. Floating Tree invocation enters filtering if not already filtering. These actions are available as config.core.file_picker and config.core.file_explorer in Normal/Visual and in the command palette.

Normal pickers and explorers show the same colored A/M/D/R markers as the diff file picker: green additions, yellow modifications, red deletions, and cyan renames (old -> new). Unchanged files remain visible. Status compares disk and unsaved buffer text with Git HEAD or Git-backed jj's single parent @-; it works in both Tree and List, even with git_gutter = "none". Missing tracked files remain as deleted entries; opening one uses the usual missing-file behavior. Status loads asynchronously from startup and refreshes on edits/saves, focus regain, and once per second, even while the picker is closed. Opening or reopening a picker uses the latest completed project-wide result without restarting the status query. The marker column always reserves its width, so filenames do not shift while the initial result arrives or when the checkout becomes clean. Ctrl+r rescans the normal catalog and refreshes status; status polling alone does not rescan for newly created files.

While a docked Tree has focus in NORMAL/navigation mode, leader sequences work in ordinary and diff views: Space o replaces the dock with the floating picker, and Space e returns focus to the document like Escape while leaving the dock visible. Invoking Space e again from the document refocuses it. Tree/selection navigation overrides retain priority. In INSERT/filter mode and List query fields, printable leader keys remain literal query text rather than triggering editor actions; configured control-key action bindings remain available.

The floating explorer is centered, capped at 100 columns by 24 rows, and shrinks to fit smaller terminals. The tree mode appears in the title; the help row does not repeat it.

The dock defaults to the right of the editor workspace. See configuration preferences to choose another side. Status and command/search prompts remain full-width; editor text, gutter, highlights, and cursors use the remaining rectangle. Focused panes prioritize their query on tiny terminals; unfocused docks shrink or hide to preserve editing space.

The explorer root path wraps within the pane width in floating, docked, and diff file views; file rows and keybinding help do not wrap. The path header grows to fit where space permits, reserving at least one file row. Very short panes clip the remaining path rather than hiding file navigation.

Both presentations default to Tree; configuration preferences can select List independently for each. Ctrl+t toggles the current focused instance without rescanning. List retains searchable full relative paths. Tree shows folders first, two-cell indentation, and basename-only files. Folder names end in / and use cyan text; files use the normal foreground color. Fold-state arrows are omitted. Maximal single-child directory chains share one row, such as src/client/http/; a directory containing one file still gets its own row.

Floating Tree opens in explorer INSERT mode; a new docked Tree opens in NORMAL. Press i in NORMAL to resume editing the existing filter; the i itself is not added to the query. Typing and Backspace filter full relative paths by case-insensitive whitespace tokens. These are explorer-local modes, not changes to the file buffer's mode. Matching nodes appear with ancestor context, and query changes initially select a direct match. Escape in INSERT changes only to NORMAL: the filter, visible results, highlighted item, and explorer focus remain. NORMAL displays the active filter and navigates that filtered tree. Escape from NORMAL returns focus to the buffer (or closes floating search). List always accepts query typing immediately.

Search reuses the diff revision selector's fuzzy matcher in both Tree and List, including changed-file explorers. Each whitespace-separated token must match an ordered character subsequence of the full relative path; tokens can appear in any order. For example, sr/fp RS finds src/fuzzy_parser.rs. Catalog order and folder context stay unchanged rather than ranking the results.

File-view input Behavior
Type / Backspace Type query text in List/explorer INSERT / remove one whole grapheme; explorer NORMAL uses hjkl for navigation
j/k (NORMAL); Ctrl+j/k or Up/Down Wrap forward/backward through visible rows
l (NORMAL); Ctrl+l/Right NORMAL expands/enters a folder; INSERT selects its visible child without changing the filter
h (NORMAL); Ctrl+h/Left NORMAL collapses/selects parent; INSERT selects its visible parent without changing the filter
Enter Toggle a folder, or open the selected file using existing buffer deduplication
i (explorer NORMAL) Enter explorer INSERT/filter mode without inserting i
Esc INSERT → NORMAL with the filter and selection retained; NORMAL → buffer focus or floating close
Ctrl+u Clear the filter in NORMAL or INSERT without changing mode/focus; preserve the selected path where possible
Tab Return to editor focus from a dock
Ctrl+w Close either presentation
Ctrl+r Rescan while retaining query, surviving expansion, and selection
Ctrl+p Open the command palette, closing floating search or blurring a dock
Ctrl+t Toggle List/Tree while retaining view state

Opening a file from a dock leaves it visible and returns focus to the buffer. Space e refocuses it. Choosing a folder during filtering returns to explorer NORMAL and toggles that folder; it never opens a text buffer for a directory. Selections use actual OS-path identities across filtering, view switches, and refresh; a removed or hidden selection falls back to the nearest visible ancestor, then the first row. Query input never edits the underlying buffer; bracketed paste is ignored while file search is focused. Cancellation preserves buffer mode, selections, and pending Insert history. Invalid, binary, unreadable, or disappeared file selections report an error and keep focus without changing the active buffer.

Clearing returns to the full catalog and reveals the selected file's ancestry in NORMAL when needed. With no current result, it uses the last available selection; removed paths fall back to a visible ancestor, then the first row. Clearing an already empty query is harmless. Filtered folders can be collapsed/expanded in NORMAL without losing the query. Temporary filtered collapse overrides survive mode/view changes and refresh of surviving directories; editing or clearing the query resets them so new matches can be revealed. Choosing a directory during filtering transitions to NORMAL and toggles it while retaining the query.

The project root is the working directory captured at startup. Scanning uses the ignore crate for nested .gitignore rules and negation; parent/global Git ignore rules are disabled. Hidden regular files and directories are included unless ignored, .git/.jj metadata is always excluded, and symlink files/directories are not followed. Files open using their original OS paths, not sanitized display labels. Root scan failures report an error; descendant/ignore errors retain reachable entries with an explicit incomplete-scan warning. A failed refresh keeps the prior catalog. There is no background watcher: refresh explicitly after external file changes.

Repository diff

Run :diff in Normal mode, choose Before, then After, and press Enter to compare. The command palette also exposes Diff; there is no default keybinding. Repository discovery starts at the active file's directory, or the startup project root for an unnamed buffer, and prefers jj over Git.

The floating selector has Revisions / Bookmarks tabs for jj, or Commits / Branches / Tags for Git. Both Before and After support case-insensitive fuzzy search by revision/change ID, commit ID, reference name, or description: f1205pas can find sm/fin-1205-pilot-assignment-schema. Space-separated tokens must all match, but can match different fields in any order; a single token cannot span unrelated fields. Exact, prefix, and contiguous matches rank ahead of scattered subsequences; otherwise equal matches favor names/IDs over descriptions, word boundaries, and tighter spans. Ties retain catalogue order, and an empty or whitespace-only query restores the original order. Working checkout stays first in every category, even when it does not match the query. As Before it is a disk snapshot, including eligible untracked files. As After it includes unsaved regular-text buffers and enables live editing; opening a comparison never saves them.

New comparisons default to Branch changes: Before becomes the unique merge base of the selected Before and After revisions, and After stays the selected tip. Changes exclusive to the Before branch are excluded. The Before border identifies both selected names and the immutable merge-base commit ID. Snapshots compares the two selected snapshots directly. Ctrl+b toggles the mode, also available as :diff.mode.toggle, the palette action, and remappable config.core.diff.mode.toggle. Toggling an opened comparison recomputes using the original selected endpoints. Selecting Working checkout on either side immediately forces Snapshots; Branch changes is unavailable while it is selected. Missing or multiple merge bases produce an explicit, recoverable selector error rather than silently falling back; choose Snapshots to compare such histories. jj's synthetic root is the empty ancestor of every jj history.

Selector input Effect
Tab / Shift+Tab Next / previous category
Up / Down, Ctrl+k / Ctrl+j Previous / next entry
Backspace / Ctrl+u Remove a query grapheme / clear query
Ctrl+b Toggle Branch changes / Snapshots (Working checkout requires Snapshots)
Enter Choose the entry
Esc Close either selector stage, cancel outstanding work, and restore the source buffer without discarding edits

Printable input belongs to the focused query, including :, _, and |; Normal-mode command or pane bindings cannot steal it. Explicit selection-navigation bindings still take priority. Use Ctrl+p to select an action, or configure a control-key action binding, when you need an editor action while the selector is focused.

The selector initially lists local revisions and known tracking refs without contacting remotes. Load refs from origin (or another configured remote) in Bookmarks/Branches/Tags lists advertised references, including refs never fetched locally, without advancing the selection stage. Selecting a reference such as some-bookmark@origin reuses available content or fetches the missing selected reference into a temporary comparison repository. Load history from origin in Revisions/Commits explicitly imports searchable remote history. Network operations use the repository's existing credentials; errors stay in the selector. They do not update active refs, snapshot jj's working copy, or change the checkout.

Pull requests: run :pull_requests [remote], use the palette's pull_requests action, or bind config.core.pull_requests. The floating picker has Open / Closed / Drafts / All tabs. Open excludes drafts; Drafts contains open draft requests; Closed includes merged requests and closed drafts. Tab/Shift+Tab changes category, typing fuzzy-searches its number, title, author, state, and branches, and Enter immediately opens the selected request in Branch changes mode using its immutable head and merge base. Escape restores the source buffer. A failed list can be retried with Enter. The default remote is origin, or the sole configured remote; specify a name when there are multiple remotes without origin.

Each pull request's metadata shows added/deleted line counts after its state, before the branch names: author · open · +123 -45 · topic → main. Zero counts are shown explicitly; missing, denied, or failed statistics show size unavailable without preventing selection. Counts come from one batched read-only statistics query per nonempty GitHub/GitLab page (GitHub, GitLab); Gitea supplies them directly in its list response. GitHub statistics require API authentication even for public repositories. Listing does not fetch source diffs.

Immediately before #number, 🙏 marks an outstanding personal review request and 👍 marks your active approval, separately from the > selection arrow. A review request takes precedence when both apply, including a re-review request after an earlier approval. “You” means the account authenticated to that forge's API—not the repository's Git author or SSH identity. Other users' and team-only requests do not count; completed or removed requests lose 🙏.

A later request for changes or dismissal removes 👍; comment-only and pending reviews do not replace an existing approval. The mark is not the PR's overall approval decision or a guarantee that you reviewed its current head; the forge controls approval resets after new commits. Customize either symbol with config.pull_request_review_requested_symbol and config.pull_request_approved_symbol; an empty string hides that symbol. Multi-cell symbols remain aligned, and decorations are not part of searchable PR labels.

Ctrl+f cycles the review filter through All → Requested → Approved. Ctrl+s cycles sorting through Newest → Requested first → Approved first. Both use the personal review states above, even when their symbols are hidden; a re-review request belongs to Requested, not Approved. Filters combine with the selected category and fuzzy query. Review priority sorts ahead of fuzzy relevance; within each group, fuzzy ranking is unchanged, or newest creation time first when the query is empty (higher PR number breaks date ties). Sorting and filtering preserve the selected PR when it remains visible, including as pages arrive or metadata changes. Category counts still report all loaded requests, and partial-search notices still apply. These controls only use already-loaded metadata and make no additional API requests.

Review-state reads reuse the same API credentials as listing. GitHub matches the current list's requested reviewers and filters published reviews by author and decision state. GitLab filters approvals by the authenticated approver and reads individual reviewer interaction states in separate bounded, paginated batches; unsupported or denied reviewer-state fields do not discard known approvals or size metadata. Gitea reuses paginated review reads per PR for both states, so its pages can take longer to load.

Anonymous, denied, missing, or failed state lookups leave the corresponding indicator unavailable without preventing browsing. A blank is not proof that no request or approval exists. Reopen the picker to fetch current review state. No approval, review request, or other remote write is performed.

GitHub, GitLab merge requests, and Gitea are supported. API reads require curl; public repositories work anonymously. Authentication uses GH_TOKEN/GITHUB_TOKEN for github.com, GH_ENTERPRISE_TOKEN/GITHUB_ENTERPRISE_TOKEN for GitHub Enterprise, GITLAB_TOKEN for GitLab, and GITEA_TOKEN for Gitea. With no matching environment token, GitHub/GitLab reuse gh auth token --hostname HOST / glab auth token --hostname HOST when available. Tokens are supplied to curl on stdin, not in subprocess arguments, and sent only to the HTTPS API; redirects are not followed. Git object fetching uses the configured remote's existing Git/SSH credentials separately.

Standard GitHub/GitLab/Gitea hosts and Codeberg are detected from HTTPS or SSH remote URLs. For another host, set remote.<remote>.vamp-forge to github, gitlab, or gitea in the repository's Git configuration, for example git config remote.origin.vamp-forge gitea. For a non-colocated jj repository, use git --git-dir .jj/repo/store/git config remote.origin.vamp-forge gitea.

SSH aliases are resolved with ssh -G before provider detection and API authentication, honoring the effective HostName and user-specific Match settings. SSH ports do not become HTTPS API ports. Git fetches retain the original remote URL, alias, and SSH identity configuration. A failed API load shows its error without also claiming that the search has no matches; the empty-result message appears only after a successful load.

Open (including drafts) and Closed fetch their first pages concurrently, newest-first, and display each page immediately. Open and Drafts filter the same catalog, sharing its loading, completion, and retry state without extra requests. Subsequent pages load in the background, with the active category's stream dispatched first; loaded rows remain selectable. Pages request 50 entries. GitLab Closed combines separate closed and merged requests (up to 100 entries per batch). All includes open, draft, closed, and merged requests from the two streams, deduplicates requests, and sorts by creation time; background updates preserve the selected PR's identity.

Search filters loaded entries locally and explicitly indicates partial results while more pages remain. A later-page error retains loaded rows; Ctrl+r retries failed fetch streams (the active category's stream, or both from All). Enter selects a loaded row; in an empty failed category it retries loading. Escape cancels outstanding work. Selection stops listing and only then retrieves that PR's historical metadata and preserved head reference, so GitLab detail reads and Gitea ref lookups do not delay the catalog.

Fork heads are fetched through the target repository's PR refs into the existing private comparison store, without changing active refs, jj operations, or checkout files. Closed/merged requests retain historical bases and heads rather than comparing against today's branch tips. If the server no longer retains the required commits, opening the request reports an error instead of displaying a different diff. Review submission and comments are separate, not part of this picker.

Missing or unprepared historical metadata is reported when selecting the affected request; it does not block listing other requests or substitute a live branch tip.

Comparison discovery builds the changed-path/status catalog first. File text, notices, and hunks load asynchronously when a file is selected and remain cached for that comparison. Failed file loads can be retried by selecting the file again; cancelled or superseded results cannot replace the current selection. Working-checkout comparisons still capture working bytes and the historical content needed to identify changes, preserving the frozen Before snapshot and unsaved After buffers.

Conflicted jj revisions also discover files without reading every blob. Ordinary files and symbolic links retain their Git contents; selected conflicted files show jj's rendered conflict projection. Unchanged links and jj's internal merge-tree metadata do not appear as changes. Viewing a conflict does not resolve it or modify the revision.

Working checkout After follows forward checkouts automatically, checked asynchronously once per second, including inactive tabs. A descendant jj change or Git HEAD rebuilds the changed-file catalog and captures current working bytes plus eligible unsaved buffers, while keeping the original Before frozen. jj tracking uses change IDs, so snapshot/description rewrites of the same change do not advance the endpoint or prevent following its later descendants. Same, backward, and unrelated checkouts do not rebuild the catalog; live-buffer editing and external reload still work normally. Historical After never follows. Set config.diff_follow_working_copy = false to disable following.

Selected jj bookmarks follow their locally known targets automatically, checked on the same one-second interval, including inactive tabs and with config.diff_follow_working_copy = false. Local bookmarks and known remote-tracking bookmarks retain their names; explicit revision selections remain pinned. A changed commit ID, including an amendment of the same change ID, rebuilds the comparison. Branch changes also recomputes the merge base. Unrelated endpoints stay frozen, except for the existing Working After forward-follow behavior. Deleted or conflicted bookmarks keep the last successful comparison visible with an error; a later successful check clears the error. These checks never fetch, contact remotes, or snapshot jj's working copy; privately loaded remote history and advertised remote selections remain frozen.

Deleted or conflicted bookmarks unrelated to the selected endpoints do not block opening or refreshing a comparison.

Pinned After editability tracks the checkout without moving either endpoint, checked asynchronously once per second, including inactive tabs and when config.diff_follow_working_copy = false. Git matches the selected commit to HEAD; jj matches its change ID so snapshot/description rewrites of the same change remain editable. A matching After uses the shared live buffer. Switching away restores the original read-only After snapshot; switching back reuses the live buffer's unsaved edits and undo history. A failed checkout check revokes pinned After editing until a successful check restores eligibility.

Refresh retains the selected path when still present, explorer query/view/visibility, logical text-side focus, and split/unified/orientation/wrap choices; moving a Working or bookmark endpoint clears review marks, while pinned checkout-only transitions retain them. Removed selections fall back to the first changed file. Bookmark replacements preserve/clamp cursor and scroll positions as replacement content loads. Failed refreshes keep the previous comparison visible. Superseded file loads and LSP navigation cannot apply to the new comparison; snapshots still displayed in other panes or tabs remain read-only.

The changed-file explorer is docked on the right. In the default split layout, modified files show Before on the left and After on the right. Added files show only After, using the full text workspace; deleted files show only Before. Rename-only files show one empty pane, with the old and new paths in the explorer; renames with text or final-newline changes retain both source sides. Before and unmatched pinned After snapshots are read-only. Working checkout or a checkout-matched pinned After binds eligible regular UTF-8 files to normal editor buffers, reusing already-open dirty buffers and their undo history. Editable After supports native edits, completion, undo/redo, and :w. Removed files, rename-only files, binary files, symlinks, and submodules remain read-only. Closing the diff keeps its buffers and unsaved edits.

Comparisons start in split, vertical (left/right) view by default. In Normal mode, Space v cycles split/unified; Space s switches a split between vertical (left/right) and horizontal (above/below, Before above After). Canonical commands are :diff.view.toggle and :diff.orientation.toggle; both are available in the palette and as remappable config.core.diff.view.toggle / config.core.diff.orientation.toggle actions. Configure new-comparison defaults with config.diff_view_mode = "split" or "unified" and config.diff_split_orientation = "vertical" or "horizontal". Current choices survive file and tab switches; closing and reopening uses the configured defaults. Orientation stays unchanged while unified is active.

Unified uses one full-width text pane with both original line-number columns: context appears once, then each changed run shows removed (-) lines followed by added (+) lines, without alignment holes. Tab/Shift+Tab still switches logical Before/After focus, preserving native source coordinates, read-only protection, and writable After editing. The border shows the combined revisions and the focused side's editability. Word wrap, syntax/semantic and character highlighting, search, selections, excerpt expansion, and wheel scrolling work in either view.

Diff metadata lives in the text-pane borders instead of a separate info row: split panes show their own Before/After revision labels at the bottom-left; a single visible pane shows the combined before → after label there. Each pane shows its read-only/editable state at the top-right, independently of keyboard focus. Before is always read-only; After is editable only for an eligible Working-checkout or checkout-matched pinned file. The changed-file explorer and panes start directly below configured chrome, and there is no native-buffer help footer. Loading/error/file notices remain visible when applicable. Narrow borders prioritize state and clip titles without painting over the gutter or source text.

Default diff tab titles show Diff · filename +N -M, with added and removed line counts for the selected file. Replaced lines count on both sides; unchanged context does not count. Counts update with live edits and undo/redo, including when the diff tab is inactive. Revision selectors and files still loading omit per-file counts until their text diff is ready. Custom chrome templates use {view}{name}{changes} for this layout.

The default top-right :diff segment shows Diff +N -M across the entire active comparison, including unopened and filtered-out files. Unopened files use native Git text statistics from frozen snapshot blobs; loaded files use their displayed hunks, including live edits. Binary and mode-only changes contribute no text lines. Git's binary detection and diff algorithm can differ from the renderer for non-UTF-8 content, late NUL bytes, or difficult comparisons, so opening such a file can adjust totals. Oversized blobs, unmaterialized jj conflict projections, or failed statistics collection leave totals as Diff … until all counts are known; partial sums are never shown as complete totals.

For jj-managed repositories, revision entries and border labels display only the change ID; Git repositories keep commit-ID labels, and named bookmarks retain their names. The backing commit ID remains the exact comparison target and can still be used to filter revisions.

Text panes use normal buffer interaction regardless of writability: motions and counts, search, Visual/Visual Line selections, multiple carets, yank, remapped bindings, and non-mutating actions work in Before and After. Read-only snapshots reject insert/delete/change/paste, repeat edits, undo/redo, mutating plugin effects, and saves, including saves to another path. Their text retains the revision's final newline. Snapshots use separate virtual LSP document identities and do not appear in ordinary buffer lists or dirty-buffer comparisons. Closing the diff disposes unreferenced snapshots and restores the live or original buffer in its workspace pane; snapshots displayed by other panes or tabs remain read-only until closed. Explicit buffer commands and Markdown/plugin navigation retain their ordinary behavior; native LSP definitions use the routing below.

LSP definition, hover, and diagnostics work normally in focused Before and After text panes, including historical snapshots. Each snapshot is served with its immutable revision text and its own document URI, not the current disk contents. Completion and completion edits are available only in writable After; they use the shared buffer's normal undo history. Revision selectors and the changed-file explorer are not document-action targets.

Accepted LSP definitions center the selection's caret row once in the destination pane, including wrapped visual rows and revealed diff excerpts. Centering uses the focused pane's actual height in either split orientation or unified view. Subsequent motions resume the configured scroll_distance policy; failed or superseded definitions preserve the current logical viewport.

A definition whose source range overlaps an actual added, removed, or replaced line stays in the same comparison and selects the correct file and side, loading file details on demand. For a location link, the symbol's full target range determines overlap and its selection range determines the caret: an unchanged declaration with a changed body still belongs in the comparison. Source coordinates belong to that side, including old paths for renames; navigation reveals the exact selection row, even when previously omitted, never the nearest excerpt. A wholly unchanged target opens a new ordinary tab, even when it is visible context or omitted text in a changed file. Files outside the comparison's changes also open in a new tab. The original comparison, pane layout, source caret, excerpts, and dirty buffers remain intact. Loaded targets share their existing text and undo history rather than reloading disk; references to snapshot URIs retain the historical text and read-only identity in the new tab, even after the comparison closes. Unreadable files, invalid UTF-8, invalid source ranges, and unsupported remote URIs do not create a tab or disturb the source.

Use the ordinary file-picker/file-explorer actions and bindings: Space o opens a floating picker, Space e opens/focuses the docked explorer, and Ctrl+w closes the focused picker/explorer and reclaims its width. Remapped bindings work too; Ctrl+e is no longer reserved by the diff. The changed-path catalog retains A/M/D/R markers and the normal compact tree, filtering, folder expansion, and configurable tree/selection navigation. It includes removed and historical paths without scanning the working directory. Enter toggles a folder or opens the selected comparison file and focuses After when that pane exists; deleted files and other Before-only views focus Before. While the explorer is hidden, Tab/Shift+Tab cycles between visible text panes only; a single-pane view keeps its focus.

File rows show colored A (added, green), M (modified, yellow), D (removed, red), or R (renamed, cyan) markers in Tree and List views, including filtered results. Rename rows show R old/path -> new/path; directories have no file-status marker.

Mark or unmark a diff file with c while browsing the changed-file tree, :diff.review.toggle, or the command-palette action. With the changed-file explorer focused, it toggles the highlighted file without opening it; from a text pane, the command toggles the displayed file. Reviewed files show a green ✓ immediately left of A/M/D/R, separated by one blank column, in Tree and List views. The modification indicator keeps its existing indentation; the review marker uses the preceding gutter space instead of adding padding. Diff rows use the selection background and cursor rather than a > prefix. Directory review marks cannot be toggled directly, and ordinary filesystem pickers have no review marks. While filtering, including List view, c remains query text; use the palette or a control-key binding there. Explicit bindings take precedence over the default browsing shortcut.

Collapsed folders show a green ✓ when every changed file beneath them is reviewed, including nested files and descendants hidden by filtering. Expanded folders stay unmarked. Folder marks update automatically when a descendant's review mark or the changed-file catalog changes.

Review marks are saved with the selected local review and restored by immutable per-file Before/After identities and file paths. Changes to unrelated files or commit metadata do not clear them; changes to either side's bytes, final newline, file mode, or rename paths require re-review. They survive filtering, file navigation, lazy loading, tabs, split/unified/orientation changes, and pinned checkout-only transitions. Editing a writable After file clears only that file's mark and saves the removal; unchanged refreshes and saves preserve it. Marks on live text also retain its exact contents, including the final newline, so different unsaved text cannot inherit a checkmark. Marks do not change document text, undo history, or repository state. Without configured SQLite storage, use :diff.review.save path to save them as JSON. Older revision-pair-scoped marks remain stored but must be marked again to acquire per-file identities; comments, summaries, and verdicts are unaffected.

Local review comments share local storage with the file-reviewed checkmarks above; neither feature publishes to GitHub. Focus either diff text pane and run :diff.comment to comment on the current line. A characterwise Visual selection (v) anchors its exact text, including partial lines and multiline spans; Visual Line (V) anchors whole selected lines. Type :diff.comment and press Enter, or use the palette/a configured binding. Multiple independent carets/ranges must be reviewed separately.

Comments, summaries, and the review overview open in a focused floating editor over the whole diff text workspace, not a source-side split. The source layout, file explorer, and viewports stay unchanged. Normal editing, selections, and undo/redo work in editable review buffers. Escape in Normal mode or :pane.close dismisses the window and returns to the diff, discarding that editor's unsaved text. An unsaved new comment is not attached to the review; an existing comment or summary reopens with its last saved text and no discarded undo history. Insert/Visual/Command Escape first retains its usual modal behavior. Source rows are never replaced with comment text. Magenta ○ gutter markers identify saved comment ranges; ● identifies the selected comment. Its exact current text range has a magenta background, including wrapped continuations and selected newline cells; native Visual selections take precedence. Stale comments have neither markers nor text highlighting. Markers preserve addition/deletion signs, appear only on the first wrapped segment, and also work in unified view.

Local review buffers (comments, summaries, overview, and stored-review selector) soft-wrap by default without inserting newlines into saved text. Use :word_wrap.toggle to disable wrapping for the focused review buffer; reopening it retains that choice for the session. Source diff wrapping remains independent.

Command Effect
:diff.comment Add a comment on the current line or exact selected text range, on the focused side
:diff.review.edit [id] Edit an existing comment; defaults to the overview entry under the caret or selected comment
:diff.review.delete [id] Delete that draft comment
:diff.review.resolve [id] / :diff.review.unresolve [id] Mark a comment resolved/unresolved; uses the same default selection as edit/delete
:diff.review.next / :diff.review.previous Cycle comments, opening their text and revealing a matching source anchor
:diff.review.overview Refresh/open a read-only summary and comment list; Enter edits the entry under the caret
:diff.review.copy Copy all saved comments in the selected review to the configured clipboard, using the CLI text format
:diff.review.summary Compose the review summary
:diff.review.verdict looks_good Record Looks good
:diff.review.verdict changes_requested Record Changes requested; none clears the verdict
:diff.review.save [path] Commit to SQLite; with a path, also export JSON for sharing
:diff.review.open [id or path] Show stored reviews, open a stored review ID, or import a JSON review as a new review
:diff.review.new Create a separate stored review for the current repository

Comment and summary text is committed only by :w or :diff.review.save, which saves the entire review, including hidden drafts. Dismissing an editor discards its unsaved text; switching between review editors can retain hidden drafts but does not save them. File checkmarks, verdicts, deletions, and manual resolution changes commit immediately to local SQLite without committing draft text. :diff.review.save path or :w path additionally exports JSON; later metadata autosaves still go to SQLite and leave that export unchanged. Imports create a new stored review identity rather than overwriting another review.

:diff.review.copy copies the same text as vamp --review show ID --all: repository, summary, verdict, counts, and every saved comment with its anchor and exact excerpt. Resolved and stale comments are included, across all files in the selected review. clipboard_mode chooses the system clipboard or Separate-mode internal register. Copying does not save or include unsaved comment/summary drafts. No comments or a clipboard write failure reports an error without replacing the register.

From the shell, use vamp --review delete REVIEW_ID COMMENT_ID [--repo PATH] [--json] to delete a stored comment immediately without opening the editor. Find IDs with vamp --review list and vamp --review show REVIEW_ID --all. The repository defaults to the current checkout. Both resolved and unresolved comments can be deleted; the review and any JSON exports remain intact. An editor holding the old review revision must reload before saving.

Storage defaults to $XDG_DATA_HOME/vamp/reviews/reviews.sqlite3, or ~/.local/share/vamp/reviews/reviews.sqlite3 when XDG_DATA_HOME is unset or relative. Configure review_directory to override its directory; relative configuration paths resolve against the config file. Repository identity is the canonical checkout root. Each review has a persistent database ID independent of comparison revisions, so reopening changed revisions retains stale comments. Opening a comparison automatically restores the repository's sole review. Multiple reviews open a floating selector: move to an entry and press Enter, or use :diff.review.open id. Use :diff.review.new for another independent review.

Write errors, locked databases, incompatible schemas, and conflicting edits from another editor instance retain dirty drafts and report the failure. Failed text saves leave the committed review unchanged; unrelated metadata autosaves cannot commit those drafts. Normal destructive closes remain guarded until drafts are explicitly saved or discarded. :w! does not bypass database revision conflicts; export your draft before reopening the current stored review. JSON exports retain external-file-change protection. Only the current database schema is accepted; existing incompatible databases are preserved rather than reset or migrated.

The current SQLite format is schema 3: typed reviews, comments, and reviewed_files tables, not serialized JSON documents. Older databases are incompatible and are left untouched; there is no migration or automatic reset. JSON version 4 is the sharing format, independent of the database schema.

Version 4 JSON is the agent-readable sharing format: repository root, summary, optional verdict, comments, and a required reviewed_files array. Each reviewed file stores its immutable per-file revisions pair (file: identities encoding mode, Git object ID or exact data, and notices), relative path, optional rename old_path, and optional exact live after text. Comments have stable IDs, bodies, required boolean resolved states, and anchors. New comments are unresolved. Database review IDs and optimistic storage revisions stay internal. Each anchor retains its immutable comparison revision, side, relative path (old path for renamed Before files), and exact whole-file text including the final newline. start and end contain zero-based source row and extended-grapheme column coordinates; end is exclusive. Columns count complete Unicode graphemes, not bytes, code points, UTF-16 units, or screen cells. Overview/window labels display one-based rows and columns. Older JSON formats are rejected and preserved without migration. Working comparisons use content-addressed snapshot IDs; captured text additionally freezes live unsaved edits. Stored reviews have a 64 MiB aggregate text budget including 64 bytes of overhead per comment and reviewed file; imported/exported JSON files have a separate 64 MiB serialized-size limit.

Reopening requires the same repository root. Unchanged comment ranges follow edits elsewhere in the file, including inserted/deleted lines above and column shifts before a partial-line selection. Working-snapshot refreshes can retain these attachments; unrelated immutable revisions and paths do not match. Mapping requires an unchanged, unambiguous text region containing the selection. Modified/deleted selections or ambiguous repeated regions are stale: comments remain available through the overview and next/previous navigation, with original snapshot coordinates and no current-source marker or jump. Saved anchors and exported excerpts remain unchanged. Unloaded files are shown as not checked until visited; refresh the overview to update its statuses. There is no server, authentication, or review-publishing operation.

Resolution is a manual human decision, independent of anchor freshness: a comment can be both resolved and stale. Overview entries and comment titles label resolved/unresolved status. Resolved comments remain in the overview and next/previous navigation, and can still be edited or reopened. Resolving never changes an anchor or deletes feedback.

Text panes show change excerpts with 10 unchanged lines above and below each change, preserving original file line numbers. Overlapping or touching excerpts merge; omitted stretches show an unchanged-line count in both panes. Files with only metadata changes show their notice or “No text changes” rather than unrelated source.

Scope labels identify named enclosing functions/classes and containers, such as impl Review › run. Muted, unnumbered rows supply context when the declaration is vertically outside that pane's viewport; scrolling it back into view removes the label and reclaims its row. If an unwrapped declaration remains vertically visible but its symbol is clipped horizontally, its label instead overlays the preceding content row. On the first visible row, that label is hidden so the pane border stays intact. Horizontal scrolling never inserts a row or shifts source text; scrolling the symbol back into view restores the covered row. Decorators alone do not count as the declaration name. Visibility follows source columns and wrapped segments independently for each side. Each split side uses its own source, so renamed functions keep distinct old/new names. Unified labels use − for Before, + for After, and ± for shared names. Narrow panes prefer the innermost name. Labels are presentation only: motions, selections, copying, editing, and line jumps retain original source coordinates. Context comes from the existing Tree-sitter trees, including supported injected code, without requiring an LSP; unsupported syntax gets no guessed label. When the reserved sticky row can show the immediately preceding declaration with its name visible, it renders that declaration as ordinary numbered source instead. Scrolling farther into the body replaces it with the unnumbered label; scrolling back restores the declaration. Overlays never cover a visible declaration name on the preceding row, including a different Before/After declaration in unified view; the label is hidden instead. Within an excerpt, additional hunks and alignment gaps do not repeat an unchanged scope label. A new excerpt or an actual scope change gets a fresh label when needed; sticky context is retained independently for each side.

In Normal mode with a text pane focused, [ expands above and ] expands below the excerpt containing the caret, revealing up to 15 lines per keypress in either pane. In editable After's Insert mode these keys insert literal text. Expansion clamps at file boundaries and merges excerpts when they meet. Existing visible source stays anchored. Opening a file resets its excerpts to the configured initial context. Live edits recompute changes while retaining expanded context; the caret stays visible even when the last text change is reverted.

Live typing highlights only the displayed source rows while retaining each snapshot's original syntax tree for multiline and injected-language context. Line-width caches and unchanged hunk layouts avoid repeated full-file work; edits that change line alignment or revert changes recompute the diff. Already-queued printable Insert keys share a redraw, but every key still passes through normal bindings, snippet handling, and undo history. Mode changes and other terminal events retain their order.

Text-pane navigation stays within the shown source lines. At an excerpt boundary, Up/Down or j/k jumps directly to the next/previous excerpt, and counts count visible lines rather than omitted lines. This also applies to remapped movement keys, Visual-mode movement, and editable After's Insert-mode arrows. Word motions, gt/gb, search, and occurrence selection use the visible excerpts; gt/gb target their first/last source line. Hidden short lines do not clamp the caret column. The shared vertical viewport and each pane's horizontal offset stay fixed when movement reverses, scrolling only when the caret crosses a visible edge, as in a normal buffer. Ordinary motions, search, and caret restoration do not reveal omitted context; Normal-mode [/] expands existing excerpts. LSP definition routing can additionally reveal the exact selection of a symbol whose range overlaps changes. Edits still recompute changed regions and retain previously expanded context.

Configure these counts in Ruby with config.diff_context_lines = 10 (nonnegative; 0 shows changes only) and config.diff_expand_lines = 15 (positive).

Diff views inherit the originating buffer's word-wrap setting. word_wrap.toggle switches both panes together, including read-only snapshots, without changing ordinary-buffer settings. Each aligned source row uses the taller side's wrapped height; shorter-side continuation padding stays blank so subsequent source lines remain aligned. Excerpts and explicit expansion still use source-line counts.

In split diffs, changed text rows use muted red backgrounds in Before and muted green backgrounds in After, including blank tails and real empty lines. Entirely added or deleted files retain the normal background; colored line numbers and +/- gutter markers indicate their changes. Foreground colors use the editor's bundled Tree-sitter syntax highlighting, parsed independently for each snapshot and its path (including the old path for renames). Native text panes also display their buffer's selections, search matches, semantic highlighting, and diagnostics. Unknown languages retain the normal foreground; historical snapshots do not borrow live-buffer LSP tokens.

The row tint also covers the line-number gutter on wrapped continuations and blank wrap-padding rows on the shorter side of an aligned pair. Empty source lines keep the same full-row tint; missing source counterparts retain their normal-background hatching.

Character-wise highlighting is enabled automatically within paired changed lines, with a stronger red/green background over changed grapheme spans. To reduce fragmented highlighting, changes separated by at most two unchanged graphemes are merged; short unchanged islands between large edits are also absorbed using VS Code-style size heuristics. Larger common regions retain the muted row tint. Combining sequences, wide characters, and expanded tabs keep their source geometry through horizontal scrolling and word wrap. Syntax/semantic foregrounds remain intact, and cursor/search/selection styles keep their normal precedence. Character ranges are cached with aligned rows; stable live edits refresh only the edited paired lines rather than computing character diffs during painting.

Diff backgrounds blend the terminal's default background with its red/green palette colors: an 18% accent for changed rows and a stronger 38% accent for changed characters, both computed from the original background. Read-only OSC 11/4 queries run after the initial paint, on focus regain, and whenever a subscribed DEC 2031 color-scheme notification arrives. While a diff is visible in any pane, a nonblocking one-second palette probe also catches external theme changes without a notification, including changes between two themes with the same light/dark preference. Ordinary editor views do not poll; cached colors apply across editor panes. If the complete palette is unavailable within 250 ms, change backgrounds remain normal and gutter colors/markers still identify changes.

Missing counterparts use adjacent muted ╱ diagonals (U+2571, box drawings light diagonal) on the normal terminal background, one per column, with the same terminal DarkGray foreground as ordinary gutter text. Unlike the text slash /, this glyph is designed to reach cell corners so diagonals can join end to end across successive rows. Pixel-perfect joins depend on the terminal's font and line-spacing rendering. Actual empty source lines retain their normal/change background and are not hatched.

In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Switching Before/After synchronizes the primary cursor by aligned diff row and display column. A missing counterpart uses the nearest visible source row; shorter lines clamp the column while retaining the preferred display column for the return trip. Vertical wheel input scrolls the visible panes without moving the focused text caret; switching sides by vertical wheel also synchronizes the cursor. Over Files it navigates visible tree rows without opening them. Horizontal wheel input in either split synchronizes the Before/After horizontal offsets, bounded by the longer side, without changing focus, cursor, or selections; over Files it is consumed. Added/deleted files retain single-pane scrolling. While a comparison is open, Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; it leaves the comparison open. Use :diff.close, find diff.close in the palette, or bind config.core.diff.close to leave the comparison explicitly. The dotted command is intentional; there is no :diffclose alias.

Both diff panes apply available semantic tokens from their own buffer, independently of keyboard focus. Historical snapshots never borrow the working buffer's tokens. Typing and undo/redo retain semantic colors outside the edited range, shifting their positions to follow the text until a current LSP reply replaces them. Edited ranges fall back to Tree-sitter syntax colors while waiting; semantic roles elsewhere can briefly lag changes. Semantic colors and modifiers preserve the change backgrounds.

Binary data, missing final newlines, permission/type changes, symlink targets, and submodule pointers have explicit notices. Symlinks are not followed and submodule contents are not traversed. jj conflicts use jj's file-content projection and carry conflict notices; ordinary files remain readable. Identical-content moves are shown as renames; modified moves appear as an added and removed file.

Large comparisons load each selected side independently, up to 64 MiB and 4,000,000 source lines per side. Frozen Working snapshots have a 512 MiB aggregate budget. Exceeding a limit displays the actual error in the document pane. Large files use the normal plain-text syntax fallback; pathological comparisons may show broader replacement regions or whole-line highlights rather than block input searching for a minimal diff. Full source text remains available.

Jujutsu conflict resolution

Run :resolve in Normal mode for the current jj working copy. It opens a dedicated tab and selects the current conflicted file (otherwise the first conflict). Historical comparisons remain read-only; resolving never implicitly checks out or creates another change.

The resolver shows the editable Resolution on the left and a read-only Other version on the right, with aligned rows, line/character highlighting, context folds and synchronized scrolling. Both are projected from the current working buffer, including pre-existing unsaved edits: only conflict-marker regions are replaced by their selected alternatives. Already-merged text and edits outside those regions appear on both sides. The initial output is seeded from the first side as a normal undoable buffer change; opening never writes it to disk. References remain frozen while later edits change only the output.

Pane borders label the editable output Resolution and its frozen reference Other version. Paths and branch-comparison metadata are omitted from these titles; unsaved state belongs only to the output, and source identity appears with the reference. Explorer C marks files jj still considers conflicted, including while edited/checking or confirmation is unavailable; it clears only when jj confirms resolution.

Command Behavior
resolve.next / resolve.previous Jump between differing decision hunks
resolve.source [N] Select/focus an Alternative; in History, select an outer input; without N, cycle
resolve.base [N] Open History and select a numbered base; default first base
resolve.use_source [N] Take the current differing hunk from that Alternative as one undoable edit
resolve.next_file / resolve.previous_file Open another retained file
resolve.files / resolve.draft Toggle/focus the file explorer / return to the editable Draft
resolve.history / resolve.view.toggle Show read-only input/base/input History / switch views
resolve.close Close the resolver without discarding dirty drafts

History is separate from editing: it compares the original inputs and their base, with all sides and bases selectable for multi-sided conflicts. config.resolve_history_layout = "columns" (default) shows left/base/right; "base_below" shows left/right above a full-width base. Both layouts retain aligned changes and synchronized vertical scrolling. Tab/Shift+Tab cycles in layout order; directional pane actions follow the displayed geometry. Narrow History shows the focused source, as does base-below History when too short to display both rows of panes. Draft text, undo, caret and viewport survive view switches. Resolution uses the ordinary diff workspace, including its configured explorer, split/unified/orientation controls, source-line navigation, excerpts, focus and scrolling. Read-only Alternatives and History reject edits and saves.

Input indexes are one-based and include bases. Conflict-specific actions appear in the palette and through corresponding config.core.resolve methods. File navigation uses the existing picker/explorer actions: Space o opens floating search, Space e focuses the dock, Ctrl+t switches Tree/List, Ctrl+w closes it, and configured tree/selection bindings apply unchanged. Enter selects a relevant conflict file and focuses the editable Draft. Normal-mode Tab/Shift+Tab, native motions/search, context expansion and layout actions follow normal diff behavior. The complete seeded Draft can be saved even when intentional differences remain against the Alternative: those differences are not unresolved conflict markers. There is no automatic “accept both”.

:w saves and checks jj, asynchronously. Before writing, the resolver locks the working copy and validates the frozen checkout and exact on-disk file state. Checkout changes, external writes and unsafe filesystem ancestors refuse the write, including force-save. Successful writes trigger jj's normal snapshot operation, which may capture other on-disk changes. The list distinguishes unresolved, unsaved, checking, resolved and unconfirmed states. jj—not marker scanning or a manual checkbox—confirms resolution. A failed confirmation keeps saved contents and reports unconfirmed status. Resolved files remain in the session for review. Dirty drafts prevent destructive resolver/pane/tab closes.

Binary, symlink, deletion, executable-bit and file/directory conflicts use explicit whole-file input choices: choose an input with resolve.use_source, then save to apply its exact bytes and type. Symlinks are never followed. Directory replacement refuses unrelated/untracked or modified child data. Git submodule choices and legacy jj conflict storage without jj:trees metadata report unsupported errors instead of fabricating a resolution.

Opening, navigating and choosing draft resolutions do not snapshot jj; only saving does. If another jj operation changes the frozen checkout, retain your draft, close after preserving it, and reopen the resolver against the new state.

Command palette

Ctrl+p opens a searchable overlay without leaving the current mode. It lists each core action once, plus registered plugin commands, with their owner, effective bindings in the current mode, and eligible modes. Disabled or overridden bindings are not advertised; actions with no key remain available through the palette. Mode-ineligible entries are dimmed and cannot execute. Retired plugin commands disappear.

Search is case-insensitive: every whitespace-separated query token must occur in the action/command name, canonical Ex name, a shorthand alias, human-readable label, or owner. Aliases find the same action rather than creating duplicate entries. Results have a stable label/name order. Ctrl+j/Down/Ctrl+n select the next result; Ctrl+k/Up select the previous, with wraparound. Backspace removes a whole grapheme; Ctrl+u clears the query without editing the underlying buffer. Enter with no match or an ineligible selection leaves the palette open. Bracketed paste is ignored while the palette is open.

Accepting a core action executes it in the underlying mode, including Visual edits and Insert-mode actions. core.edit and core.buffer instead open prefilled Ex prompts for their required arguments. The destructive core.force_quit, core.buffer_force_delete, core.pane.force_close, core.force_save, and core.force_save_and_quit actions also open canonical Ex prompts, requiring another Enter before discarding or overwriting changes. Accepting a plugin command is available only in Normal mode and opens a prefilled : prompt; add arguments if needed, then Enter executes through the usual session dispatch.

Esc or Ctrl+p cancels without changing text, selections, the previous command/search prompt, or pending Insert history. The opening key can be remapped with config.core.palette; list navigation uses the shared configurable selection context. Other popup controls keep their defaults unless explicitly intercepted by a selection binding. The TUI uses a constraint-based modal panel, keeps the selected result visible, and prioritizes the query on tiny terminals.

Key sequences

Space begins a leader sequence in Normal/Visual by default. Explicit sequences such as ctrl+w j can coexist without changing that leader. Press the keys in order; nested prefixes wait without a timeout. The status bar shows the pending keys before the file path, without a prefix label. Esc cancels the sequence and pending count without leaving Visual or collapsing carets. See sequence configuration for remapping and declaration rules.

Jump history

Normal Ctrl+o moves back through previous locations; Ctrl+i moves forward, including in comparisons. Normal Tab also jumps forward outside comparisons, but cycles panes within comparisons; Shift+Tab cycles backward. The Unix TUI requests disambiguated keyboard events so supporting terminals preserve this distinction. Legacy terminals encode Ctrl+i as Tab and cannot distinguish them; there, use the Jump forward palette action or another configured key such as config.bind "ctrl+y", config.core.jump_forward. Counts work in either direction, such as 3 followed by Ctrl+o, and stop at the available history.

The session keeps up to 100 primary-caret locations with their tab, pane, and comparison context, not just buffer coordinates. Successful Markdown links, accepted LSP definitions, file-picker opens, :edit, :buffer, :bnext/:bprev, and search //n/N jumps record locations. Back/forward restores the original view, including its comparison file and side; an external LSP definition round-trip does not replace the comparison with an ordinary buffer. Identical buffer/cursor positions in different views remain distinct. Ordinary cursor/word motion, edits, failed navigation, manual tab/pane selection, and jumps that do not change location do not add entries. Traversal itself does not record another jump.

Moving away from a visited point before traversing saves the actual departure position for the return trip. A successful new jump after going back discards the forward branch; failed/no-op jumps preserve it. Closed buffers are skipped rather than reopened. An unavailable or replaced comparison is never resurrected or overwritten: history uses a safe existing ordinary pane for an available buffer, retaining snapshot read-only identity, or skips the destination if none exists. It never creates a replacement tab. Stored row/grapheme coordinates clamp to current text after edits; text, dirty state, undo/redo, and file contents remain unchanged. History navigation returns to Normal with one primary caret.

The typed actions are config.core.jump_back and config.core.jump_forward; custom bindings can use any supported single key or leader sequence. History is in memory for this session only.

Normal gf or :follow_link follows the link under the primary caret, without an LSP. Inline links and full/collapsed/shortcut reference links are supported; reference definitions can appear elsewhere in the document. The label or destination can be under the caret. Images, code blocks, and ordinary text are not file links.

Local paths resolve relative to the source file, not the working directory; percent-encoded UTF-8 paths and Markdown-escaped punctuation are decoded. Targets can be any supported text file. Existing buffers are reused, including unsaved text and files removed from disk. Missing files, undefined references, invalid destinations, or missing headings report an error without switching buffers or moving carets.

[Section](#section) stays in the current document; [Section](other.md#section) opens the target and moves to its heading. ATX and Setext headings use GitHub-style slugs derived from visible inline text: lowercase, punctuation removed, whitespace replaced by hyphens, and duplicate suffixes -1, -2, etc. Heading lookup uses unsaved target text. Relative file links require a named source; same-document fragments also work in unnamed buffers.

External URL schemes/network paths are rejected; this action never launches a browser or shell. Numeric HTML entities and common named entities (amp, lt, gt, quot, apos, nbsp and their supported aliases) are decoded; other recognized named entities report an unsupported-entity error rather than guessing. Unicode slugs/reference matching use standard-library case conversion, not a complete Unicode case-fold/category table.

g waits for f in Normal, or t/b in Normal and Visual, shows in status, and Escape cancels. Other input cancels the prefix and follows ordinary input handling. A configured g binding overrides this built-in prefix in its mode; choosing g as leader also takes precedence. Custom bindings use the generated action:

config.bind "leader f", config.core.follow_link

Syntax highlighting

Buffers highlight automatically using Tree-sitter and compiled-in grammar/query packages. The selected language set is Rust, Ruby, Python, JSON, Bash, Makefile, Markdown, JavaScript/JSX, TypeScript/TSX, HTML, CSS, YAML, TOML, C, C++, Go, Zig (.zig), and Honey (.hon). Detection uses filename/extension or the in-memory first-line shebang, including common special filenames such as Makefile, Gemfile, and Cargo.lock; no file reread occurs. Unsupported formats use the normal foreground. The detected language appears in status, and save-as redetects it.

Files remain editable above the syntax budget: sources over 1 MiB, or with any line over 64 KiB, use plain text and show the reason in the language status. Syntax highlighting resumes automatically after shrinking below both limits. This avoids building a full parse tree for huge generated files; navigation, search, selections, undo/redo, and saving remain available.

Colors retain the terminal palette: comments dark gray, strings green, keywords magenta, numbers yellow, types/markup cyan, and functions light blue. Grammar queries handle multiline/nested constructs, even when their opening is above the viewport. Markdown uses its inline grammar and named fenced-code injections; HTML script/style content uses JavaScript/CSS. Known tagged-template and raw-string injections use the same selected grammar registry; unknown injected languages retain host highlighting or plain text. Highlighting styles whole graphemes and expanded tab cells; partially clipped wide placeholders remain unchanged. Search, Visual selections, and secondary carets retain foreground/background precedence.

Honey uses its bundled grammar and highlight queries, including declarations, primitive types, literals, and comments. Markdown fences accept honey, honeylang, or hon; the language ID is honey.

Markdown heading markers (# through ######, or Setext underlines) share the heading text's markup color. List markers (-, +, *, and ordered prefixes such as 1. or 1)) also use cyan markup, including nested and blockquoted lists. Plain-text hashes and other punctuation keep their existing styling.

An enabled supporting LSP server supplies semantic tokens automatically, overriding syntax colors only on recognized spans. Semantic colors also use the terminal palette: namespaces neutral gray, types cyan, variables/parameters light cyan, and properties blue. Readonly variables/parameters/properties keep those role colors and add italics; declarations/definitions are bold, documentation comments italic, and deprecated symbols struck through. Italic emphasis depends on terminal support. Edits retain semantic colors outside the changed range and shift them with the text until updated tokens arrive, avoiding whole-buffer flashes. Tree-sitter fills gaps, including edited ranges whose semantic tokens were discarded. Search, Visual selections, diagnostics, and carets keep their existing precedence. See native LSP configuration for provider support and server-stack routing.