61 KiB
Editing guide
Default controls and editing behavior. For custom bindings, see configuration; for language-server commands, see native LSP.
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, lists, 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 | Ctrl+o goes to an older jump location; Ctrl+i/Tab goes forward; counts select multiple locations |
| Normal, Visual | Decimal prefixes repeat cursor/word/search motions: 5j, 3k, 2w, 4e, 2n; Esc cancels a pending count |
| Normal | J (Shift+j) joins each targeted line with the next line |
| Normal | x/Delete delete; u undo; U (Shift+u) redo; : enter command mode |
| Normal | dd deletes whole lines; cc replaces them and enters Insert; leading counts work (3dd, 2cc) |
| Normal, Visual | . repeats the last successful native edit against the current targets; one undo group |
| Normal | / opens search; n finds the next match; N (Shift+n) finds the 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 | 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 | Enter executes; Backspace edits; Esc cancels |
| Search | Type a literal query; Enter finds the next match; Backspace edits; Esc cancels |
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.
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 literal Tab behavior; 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 inserts a literal tab. 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 dd/cc prefixes retain the count until their action resolves. Line actions use that count; other non-motion commands consume it and execute once, including line-start/end, other edits, and plugins. 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, and general Vim operator grammar such as 3dw is not supported.
Normal line 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.
The first d or c waits for its matching second key and appears in the status bar. Escape cancels only that pending prefix/count, preserving carets; an unmatched key cancels the prefix and then performs its ordinary action. Only leading counts are supported, not forms such as d2d or d{motion}. 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.
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.
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.
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 dd and cc, but not general operator grammar such as cw, dw, or yy; blockwise Visual is also unsupported. c and d 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 or enter Ex commands after leaving Visual mode. 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
config.clipboard_mode = "system" is the default: ordinary Visual y/d/x/c and Normal 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) 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.
System clipboard access supports macOS through osascript, Linux Wayland through wl-paste/wl-copy, and Linux X11 through 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
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. The active match is highlighted yellow.
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, clears the highlight, 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 clear the highlight but retain the query, so subsequent navigation searches the current text again.
Commands, saving, and pasted input
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], :quit, :force_quit, :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 |
:q |
:quit |
:q! |
:force_quit |
:wq |
: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. Saves use a same-directory temporary file and rename, preserve existing permissions, and reject target symlinks. Writing to a path owned by another loaded buffer refuses before modifying the file. 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.
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.
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 buffers and has 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. 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.
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 the focused pane and its exclusively displayed buffer, subject to unsaved-change protection |
:pane.force_close |
config.core.pane.force_close |
Explicitly discard an exclusively displayed buffer's changes and close its pane |
Closing a pane gives its space to its sibling:
| Situation | Close pane | Force close pane |
|---|---|---|
| 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.
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 |
Multiple tabs show a single top row with padded numbered filenames, subtle separators, and dirty markers. The active tab uses bold accent text; the row keeps the terminal's background instead of painting a full-width band. Narrow terminals keep the active tab visible. New/close actions have no default keybinding; for example, config.bind "leader t", config.core.tab.new.
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 |
List stable IDs and paths; % marks active, + marks 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, discard, or reload hidden edits. 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.
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.
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.
| 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. Type to filter by revision/change ID, commit ID, reference name, or description. 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.
| 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 |
| Enter | Choose the entry |
| Esc | Cancel loading; After → return to Before; Before → clear query without closing |
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.
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.
The changed-file explorer is docked on the right. 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 the split view. Before and historical/remote After snapshots are read-only. Selecting Working checkout as 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.
When a diff occupies the sole workspace pane, its top bar spans the full editor width. The right-docked changed-file explorer starts below that bar rather than covering the header; closing the dock preserves the full-width bar.
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 remain read-only until closed. Explicit native navigation from the comparison pane to an ordinary buffer leaves the comparison at that destination.
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 its first visible text pane: After for added files, otherwise 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. Ordinary file exploration remains unannotated.
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.
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. Moving or restoring a caret never reveals omitted context: only Normal-mode [/] expands existing excerpts. 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.
Character-wise highlighting is enabled automatically within paired changed lines: only changed graphemes receive a stronger red/green background, while common text retains 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. 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. Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; it never closes the diff. 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; live After tokens must match the displayed text revision. Missing or invalidated tokens leave Tree-sitter syntax colors intact. 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.
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. 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, and core.pane.force_close actions also open canonical Ex prompts, requiring another Enter before discarding 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. Traditional terminals encode Ctrl+i as Tab, so Normal Tab performs the same action while Insert Tab inserts text outside snippets. 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 across buffers. Successful Markdown links, accepted LSP definitions, file-picker opens, :edit, :buffer, :bnext/:bprev, and search //n/N jumps record locations. Ordinary cursor/word motion, edits, failed navigation, 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. Stored row/grapheme coordinates clamp to the current text after edits; text, dirty state, undo/redo, and file contents are unchanged by traversal. 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.
Markdown links
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++, and Go. 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.
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.
Markdown heading markers (# through ######, or Setext underlines) share the heading text's markup color. 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. Tree-sitter fills gaps and remains available while new tokens are pending after edits. Search, Visual selections, diagnostics, and carets keep their existing precedence. See native LSP configuration for provider support and server-stack routing.