Modal editing

Modal editing: Normal / Insert / Visual / Select / Command / Search / Replace, the vim grammar (operators + motions + text objects + counts), and registers / marks / macros.

On this page

Lattice is a modal editor. Different keystrokes do different things depending on which mode you're in. The modes are a small state machine; the transitions are deterministic; the grammar is vim's, with a few principled extensions.

If you've never used vim: this is the page that gets you fluent. If you have: skip to the grammar section — lattice is vim with explicit unification of : and the keymap, and a few sharper edges around modes that you'll find familiar but cleaner.


The modes

ModeCursor lookWhat keys do
NormalBlockMove + run commands; default
InsertBar (``)
ReplaceUnderlineType — overwrites instead of inserting
Visual (3 sub)HighlightSelect a range; next operator acts on it
Select (3 sub)HighlightLike Visual, but typing replaces the selection and drops to Insert
Operator-Pending (transient)BlockAwaiting motion / text-object after d/y/c/etc.
CommandBar at : lineTyping an ex-command (:w, :set, ...)
SearchBar at / lineTyping a search pattern (/foo, ?bar)

<Esc> always returns to Normal. From any text-input mode (Insert, Replace, Command, Search), <Esc> cancels and goes back. From Visual, <Esc> (or v/V/<C-v>) drops the selection.

The other axis — major + minor modes — is orthogonal to this. Modal state is your hands; major/minor modes are the editor's behaviour layers. See modes for that axis.


Quick reference: enter / leave each mode

From NormalTo
iInsert at cursor
IInsert at first non-blank of line
aInsert one column right (append)
AInsert at end-of-line
oOpen a new line below + Insert
OOpen a new line above + Insert
RReplace mode
vVisual (charwise)
VVisual (linewise)
<C-v> (or <C-q>)Visual (blockwise)
ghSelect (charwise)
gHSelect (linewise)
g<C-h>Select (blockwise)
:Command
/ / ?Search forward / backward
gvRe-select the previous Visual selection
From Insert / Replace / Visual / Select / Command / SearchTo
<Esc>Normal

Cancelling a running operation

<Esc> returns you to Normal but deliberately does not cancel work in progress — pressing it out of habit should never kill a search you have been waiting on. <C-g> is the key that stops a running operation and returns to Normal.

See cancellation for what it stops, what it leaves alone, and why <C-c> is not the cancel key.

In Visual and Select <C-g> keeps its vim meaning (toggle between the two, preserving the selection), so there is no cancel chord there — press <Esc> first, then <C-g>.

From VisualTo
v/V/<C-v>Switch among Visual sub-modes (or drop to Normal if same kind)
oSwap selection anchor and cursor (extends to the other end)
<C-g>Toggle to Select (same selection, inverted typing)
<Esc>Drop selection → Normal
From SelectTo
any printable keyReplace the selection with that char → Insert (o and i included)
<CR> / <C-j>Replace the selection with an indented newline → Insert
<C-g>Toggle back to Visual (same selection)
<Esc>Drop selection → Normal

The vim grammar

Every Normal-mode action is built from at most four pieces:

[register] [count] operator [count] motion-or-text-object
   |          |       |        |          |
   ▼          ▼       ▼        ▼          ▼
 "a         3       d         2        w
   |        |       |         |        |
 paste-     do-3-  delete    over-2  -words
 to / yank- of-                forward
 from
  • [register]"a ... "z / "_ / "+ ... — where to yank from / paste from. Optional; default ".
  • [count] — leading number repeats the command. Optional; default 1.
  • operatord (delete), y (yank), c (change), > (indent), < (dedent), gU (uppercase), gu (lowercase), g~ (toggle case), = (re-indent / format).
  • motion-or-text-object — the what the operator acts on.

Examples:

Key sequenceMeaning
dwDelete word forward
d2wDelete 2 words forward
2dwSame — count outside operator multiplies
"ayyYank current line into register a
"apPaste from register a
dawDelete a word (with surrounding whitespace)
diwDelete inner word (the word / punctuation / whitespace run at the cursor)
>>Indent current line
2>>Indent 2 lines (current + 1 below)
c$Change to end-of-line
gUwUppercase a word

In Visual mode, the selection IS the range — operators take no further argument. dw outside Visual deletes a word; inside Visual, d deletes the selection.


Motions

A motion moves the cursor. After an operator (d, y, c, ...), the motion defines the operator's range.

Cursor / character

KeyMotion
h / lLeft / right one character
j / kDown / up one logical line
gj / gkDown / up one display row (wraps count)
0Start of line (column 0)
^First non-blank of line
$End of line
g0Start of current display row (under :set wrap)
g$End of current display row (under :set wrap)
g_Last non-blank of line
ggFirst line
GLast line
42GLine 42
:42<CR>Same as 42G (jump-by-line)

Word / token

KeyMotion
wNext word start (word-class boundary)
bPrevious word start
eNext word end
gePrevious word end
WNext WORD (whitespace-separated; punctuation is part of WORD)
BPrevious WORD
ENext WORD end
gEPrevious WORD end

Sentence / paragraph / section

KeyMotion
(Previous sentence
)Next sentence
{Previous paragraph
}Next paragraph
]]Next section start
[[Previous section start

Find character on line

KeyMotion
fXJump to next X on this line (cursor lands on it)
FXSame, backward
tXJump till next X (cursor lands on the char before)
TXSame, backward
;Repeat last f / F / t / T
,Reverse-repeat
3fXJump to the 3rd X ahead

Match / pair

KeyMotion
%Jump to matching (/), [/], {/}

% finds the first bracket at or after the cursor on the current line, then jumps to its partner, counting nesting. It is a real motion, so it composes: d% deletes the bracketed span including both brackets, y% yanks it, v% selects it. With no bracket on the line it does nothing rather than hunting down the buffer — which also means a failed d% deletes nothing.

Viewport

KeyMotion
HTop of visible viewport
MMiddle of viewport
LBottom of viewport
<C-d>Half-page down
<C-u>Half-page up
<C-f>Full page down
<C-b>Full page up
<C-e>Scroll one line down (cursor sticky to viewport)
<C-y>Scroll one line up
zzCentre cursor row in viewport
ztCursor row at top of viewport
zbCursor row at bottom of viewport
KeyMotion
/patternSearch forward for pattern
?patternSearch backward
nRepeat last search (same direction)
NRepeat backward
*Search forward for the whole word under cursor
#Search backward for the whole word under cursor

* and # match the word whole, as vim does: * on foo stops on foo and skips xfoo, barfoo and foobar. If the cursor is not on a word, both take the next word on the line. Use /foo when you do want the loose match.

With a search active, the match the cursor is on gets the stronger current-match highlight. Move off it (j, an edit, a click) and only the ordinary match highlight remains; land on another match and it becomes current. :nohlsearch clears both.

n, N, * and # are motions, so they compose like any other: dn deletes up to the next match, y* yanks up to the next occurrence of the word under the cursor, and vN extends a selection back to the previous match. A search that finds nothing deletes nothing and says E486.


Operators

An operator + a motion = a deletion / yank / etc. over the motion's range.

OperatorAction
dDelete (yanks into " register first)
DSame as d$ (delete to end-of-line)
yYank (copy)
YSame as y$
cChange — delete + Insert
CSame as c$
sSubstitute char — cl (delete one + Insert)
SSubstitute line — cc
>Indent right
<Indent left
gUUppercase
guLowercase
g~Toggle case
~Toggle case on cursor char (no motion needed)
=Re-indent / format (uses LSP if lsp-format-mode active)

Doubled operators = whole-line

dd / yy / cc / >> / << / gUU / guu / g~~ all operate on the cursor's whole line. Counts apply: 5dd deletes 5 lines.

. repeat

. repeats the last change (operator + motion). Yank-only invocations don't go on the dot register; only mutating ops.

Replace characters — r / R

Overwrite in place, without deleting-and-retyping:

KeyAction
r{char}Replace the character under the cursor with {char}; stay in Normal
{count}r{char}Replace {count} characters with {char}; a no-op if fewer than {count} remain on the line
REnter Replace mode — every printable key overwrites; <BS> restores the char it replaced; <Esc> returns to Normal
{visual} r{char}Replace every selected character with {char}, then return to Normal (charwise, linewise, and blockwise selections all work; newlines are preserved)

r and Visual r wait for the next key to use as the replacement, so <Esc> there cancels the pending replace. Line breaks are never overwritten — a multi-line selection keeps its line structure.


Text objects

Text objects are the what arguments that aren't motions — they describe a structured chunk of buffer.

ObjectMeaning
aw / iwA word (with whitespace) / inner word
aW / iWA WORD / inner WORD
as / isA sentence / inner sentence
ap / ipA paragraph / inner paragraph
a" / i"Around / inside double-quoted string
a' / i'Around / inside single-quoted
a` / i`Around / inside backtick-quoted
a( / i(Around / inside parens (also ab / ib)
a{ / i{Around / inside braces (also aB / iB)
a[ / i[Around / inside brackets
a< / i<Around / inside angle brackets
at / itAround / inside an HTML/XML tag

Combine with any operator: daw deletes a word + whitespace, ci" changes the inside of a double-quoted string, ya{ yanks a brace block including the braces, >>i{ indents inside a brace block (without the braces).

In Visual mode, any text object sets the selection to its span: vi{ selects everything inside the surrounding {...}, vaw a word with its whitespace, vap a paragraph. The structural and comment objects below work the same way (vaf, vaC, …). There is no per-object Visual handling — every object in the grammar selects uniformly.

Tree-sitter text objects

Status: function / class / parameter / loop + comment objects landed 2026-06-10 (N.1.4–N.1.6); Visual-mode selection (viw / vaf / vaC / …) landed the same day in the visual-foundation slice. The query substrate (textobjects.scm + scope_at_cursor) landed in N.1.0; the zn narrow operator in N.1.3. Design: ../dev/architecture/tree-sitter-text-objects.md.

The objects above are delimiter- and whitespace-based — they don't know what a function or a type is. Tree-sitter text objects add structural objects read from the syntax tree, registered as first-class grammar objects: each composes with every operator, exactly like iw or ip. a (outer) is the whole construct (signature → closing brace); i (inner) is the body without the delimiters.

Objectouter / innerSelects
Functionaf / ifa function, method, or closure
Class / typeac / ica struct / enum / trait / impl (Rust), class (Python/JS)
Parameter / argaa / iaone argument in a parameter / argument list
Loopal / ila for / while / loop
CommentaC / iCaC the comment block incl. markers; iC the comment text (first line's leader stripped). Commentstring-driven — any language with a known line-comment leader, no parse tree needed

Combine with any operator: daf deletes a function, cic changes a class body, daa deletes an argument, yif yanks a function body, =af reindents a function, and znaf narrows to a function (the zn narrow operator). They resolve against the live syntax tree, off the UI thread.

v1 notes. Resolution is byte-precise — daa deletes exactly x: i32, not the whole signature line. Two rough edges for now: if / ic / il include the body's braces in brace languages (Python's brace-less suite is clean); and aa and ia resolve identically (the trailing comma isn't captured yet). The comment object scans line comments (//, #); block comments (/* */) and trailing comments are a follow-up. Visual-mode selection applies to every object uniformly (vaf / vaC / viw / …) — the Visual binder iterates the same grammar table the operators do, so there is no per-object code.

Why these keys. t (tag) is already a classic object, so class/type takes ac/ic (the nvim-treesitter convention) rather than Helix's t. Comment takes capital aC/iC — preserving the inner-vs-outer distinction a single gc object can't express — and leaves gc free for a future comment operator (gcc). call / conditional / block have no dedicated key yet (their natural letters clash with the classic paren/brace objects); reach them via the classic i( / i{ and via the narrow operator below.

Innermost wins. With the cursor inside a closure nested in a function, af targets the closure; move to the function's signature line to target the whole function.

The narrow operator. zn is a narrow operator that pairs with any motion or text object: znaf narrows a function, znic an inner class, znip a paragraph, zni{ inside braces, znG to end-of-file. Edits in the narrow view save back to the source file; :widen (or q) closes it. See ../dev/architecture/narrow-mode.md.

Language coverage today: Rust, Python, JavaScript ship a textobjects.scm. A language with no query simply has no tree-sitter objects (the delimiter objects above still work). Adding a language's objects is one query file — see languages.


Visual modes

Visual is "select-then-act." Three sub-modes:

ModeEnterSelects
CharwisevChar-by-char from anchor to cursor
LinewiseVWhole lines from anchor's line to cursor's line
Blockwise<C-v>Rectangle anchored at anchor, extending to cursor

Once in Visual:

  • Any motion extends the selection.
  • An operator (d / y / c / > / < / gU / ...) acts on the selection and exits Visual.
  • o swaps anchor and cursor (extends to the other end).
  • gv re-selects the previous Visual selection (Normal only).

"Any motion" means any — the single-key ones in the table above, gg, f / t and their reverses, <C-d> / <C-u>, the tree-sitter structural motions (]f, [c, ...), and motions a plugin or a major mode contributes. Org's [[ and ]] walk headlines in Visual exactly as they do in Normal, and a motion you add yourself needs no extra declaration to work here: a motion is live in Normal, Visual, Select and after an operator because it is a motion, not because somebody remembered to list it four times.

A handful of cursor-moving commands are not motions and so do not compose with an operator — <C-f> / <C-b> / <C-e> / <C-y> scroll the window, and vim does not accept them after d either.

Blockwise specials

In blockwise Visual:

  • I enters Insert at the left edge of the block; on <Esc> whatever you typed replicates on every row at that column.
  • A enters Insert at the right edge + 1; same replicate behaviour.
  • > / < indent / dedent the rectangle.
  • r followed by a char fills the rectangle with that char.

The whole replicate session is one undo unit — u once reverts the entire block edit.


Select mode

Select is Visual's twin: identical selection geometry (same anchor/head, same three sub-modes), but inverted typing. In Visual you press an operator (d, c, y); in Select you just type, and the typed character replaces the whole selection and drops you into Insert. It mirrors the "type-to-replace selection" behaviour of conventional editors, and it's the substrate vim snippet expansion uses to highlight an editable placeholder.

ModeEnterSelects
CharwiseghChar-by-char from anchor to cursor
LinewisegHWhole lines from anchor's line to cursor's line
Blockwiseg<C-h>Rectangle anchored at anchor, extending to cursor

Once in Select:

  • A motion you can't type extends the selection, exactly as in Visual: the arrow keys, <Home> / <End>, <PageUp> / <PageDown>, <C-d> / <C-u>. A motion that starts with a printable key (w, e, }, f) types that key instead; press <C-g> to switch to Visual when you want it. This holds for motions a plugin adds, too. The same goes for o (swap ends) and text objects (iw, af): in Select those keys type, as in vim.
  • Typing any printable character replaces the selection with that character and switches to Insert. So do <CR> and <C-j>, which replace it with a newline, indented like any newline you type. The replace + insert is a single undo unit — one u restores the original span.
  • <C-g> toggles back to Visual without losing the selection; from Visual, <C-g> toggles into Select. The selection is preserved across the toggle either way.
  • <Esc> drops the selection and returns to Normal (the dropped selection is remembered, so gv re-selects it).

Operators do not apply in Select — the keys that would be operators in Visual are printable characters, so they overtype. That's the whole point: Select is for "select, then type over."


Registers + macros

Registers are named buffers for yank / paste.

  • "a ... "z — named registers (lowercase appends, uppercase appends)
  • "0 ... "9 — numbered (0 = last yank; 1-9 = recent deletes)
  • "+ — system clipboard (when the platform supports it)
  • "_ — black hole (write-only sink; "_dd deletes without affecting any register)
  • "% — current buffer's path (read-only)

Use:

  • "ayy — yank line into register a
  • "ap — paste from register a
  • :reg<CR> — list every populated register

Pasting from outside the editor

"+p pastes the system clipboard with vim's placement (charwise or linewise, depending on how the text was copied). For an ordinary "paste what I just copied from a browser" there is also the OS shortcut, which inserts literally at the cursor and works in Insert mode, on the : line, on the / line and in any prompt:

WhereShortcut
Terminal (TUI)Your terminal's own paste — Cmd+V on macOS, Ctrl+Shift+V on most Linux terminals
GUI windowCmd+V on macOS, Ctrl+Shift+V elsewhere

Plain Ctrl+V is not paste — it is blockwise Visual, and it stays that way. Cmd is unused by the vim grammar, and Ctrl+Shift+V is the convention terminals already use for exactly this reason.

The two renderers reach this differently, which is why the shortcut is the terminal's own in one and the editor's in the other: a terminal sends a paste as a single bracketed burst carrying the text, while a GUI toolkit delivers the shortcut as a key and leaves the application to read the clipboard.

Pasting a copied line into the : line, the / line or a prompt flattens its newlines — those are one-row surfaces, and a newline would put text in the buffer that the row cannot show. The expanded mini-buffer band (C-x C-e) is multi-line on purpose and keeps them.

Macros record key sequences:

  • qa — start recording into register a
  • q — stop recording
  • @a — replay the macro
  • @@ — replay the last-played macro (not necessarily @a)
  • 5@a — replay macro a 5 times

Macros record CommandInvocation sequences, not raw keystrokes — so a recorded macro survives keymap changes (rebinding j doesn't break a macro that uses motion-down).


Counts

A leading number repeats. Almost everywhere:

  • 5j — 5 lines down
  • 3w — 3 words forward
  • 2dd — delete 2 lines
  • c3l — change 3 chars
  • 5x — delete 5 chars
  • 4>> — indent 4 lines

Counts can sit before or after an operator: 2dw and d2w both delete 2 words. The convention is "operator first when the count is for the motion" but they're interchangeable.


Edge cases

<Esc> from Insert moves the cursor left

vim convention. Coming back to Normal, the cursor sits on the char before where you were typing. Use a (append) to keep the cursor where it was visually.

c with a motion that doesn't change anything

Vim quirk: cw on a single-char word with no whitespace ahead is well-defined; c$ on an empty line just enters Insert. No errors, just no-op edits.

Counts with f / t

3fX jumps to the third X ahead, not "find X 3 times." f always lands on the char; t always lands one before. ; repeats with the count it remembers.

Repeating . after a count

. repeats the operator + motion + count. So if you ran 3dd, then . deletes 3 more lines. To repeat without the count, prefix . with a different count: 5. repeats with count 5.

Block-visual on short rows

When a block-visual selection's right edge extends past a short row's content, that row's contribution is silently skipped for I / A insertion (vim's behaviour). The other rows still get the replicated text.


See also

  • modes — major + minor modes (the other mode axis).
  • buffers — buffers, panes, splits.
  • folding — fold-aware motions + operators (zj/zk, dd-on-fold semantics).
  • completion — insert-mode completion + ghost text.
  • ex-commands — the : line.
  • options:set + :customize.
  • lsp — LSP feature surface.
  • tutor — interactive lesson sequence (when available).
  • docs/dev/architecture/keymap-architecture.md — developer reference for the keymap registry + dispatch.