Text Objects
Vim edits text in chunks — a word, a sentence, a paragraph, a quoted string, a block inside parentheses. Each chunk is a text object, and an operator like c (change), d (delete), or y (yank) can act on the whole chunk at once.
Text objects are the nouns of Vim's editing language; operators (c, d, y) are the verbs that act on them. You act on a whole "chunk" of text without ever counting characters or thinking about its exact boundaries.
Start Here: ciw
Learn one object before the whole system. ciw = change inner word:
| Part | Meaning |
|---|---|
c | change — delete the text, then let you type |
i | inner — the content only, not the spaces around it |
w | word — the word the cursor is on |
Read the cursor marker
In every example a ^ shows where the cursor sits — it is a picture, not text you type:
The quick brown fox
^
cursor
Here the cursor is on the q of quick.
Before → after
With the cursor on quick, press ciw, type fast, then press Esc:
Before: The quick brown fox
^
After: The fast brown fox
Result: only quick became fast. The, brown, fox, and every space stayed exactly where they were. You never had to move to the start of the word — ciw took the whole word from wherever the cursor was. That position independence is the whole point of text objects.
Test it in text-objects-practice.md (open it with the shell alias np-top):
| Command | Drill line to test | Expected result |
|---|---|---|
ciw | line 61 — The quick brown fox... (cursor in quick) | changes only quick to the word you type |
Text Object Syntax
{operator} {i|a} {text-object}
i = "inner" (without surrounding whitespace/delimiters)
a = "around" (includes surrounding whitespace/delimiters)
Every operator + i/a combination below is drilled in text-objects-practice.md (open it with the shell alias np-top), starting with Drill 0 (line 33).
The Two Modifiers: i and a
| Modifier | Mnemonic | What it includes |
|---|---|---|
| i | inner | The content, without surrounding chars |
| a | around | The content AND surrounding chars/whitespace |
Example with parentheses (hello world) — put the cursor anywhere inside the object:
(hello world)
^
cursor
ci( → change inner ( → removes "hello world", keeps the parens
ca( → change around ( → removes "(hello world)" including the parens
di( → delete inner ( → removes "hello world", keeps the parens
da( → delete around ( → removes "(hello world)" including the parens
Before → after
Cursor inside (hello world), press ci(, type bye, then press Esc:
Before: (hello world)
^
After: (bye)
Result: only the inner text changed — the parentheses stayed. ci( replaced hello world but kept the () wrapper.
Press ca( instead to drop the parentheses too:
Before: (hello world)
^
After: bye
Result: the whole (hello world) — parentheses and all — became bye.
Test each command on the drill line in
practice-folder/nvim/03-text-objects-and-motions/text-objects-practice.md
(open it with the shell alias np-top):
| Command | Drill 0 line to test | Expected result |
|---|---|---|
ci( | line 33 — (hello world) | replaces hello world, keeps the parens |
ca( | line 33 — (hello world) | replaces (hello world) including the parens |
di( | line 33 — (hello world) | leaves an empty () |
da( | line 33 — (hello world) | removes the whole pair, line becomes empty |
Word and WORD Text Objects
| Text Object | Selects |
|---|---|
| iw | Inner word — the word without surrounding space |
| aw | Around word — word plus one surrounding space |
| iW | Inner WORD — space-delimited word without space |
| aW | Around WORD — WORD plus surrounding space |
For: " hello world "
^^^^^ ← iw selects "hello"
^^^^^^^ ← aw selects "hello " (with trailing space)
For: user-profile.avatar
^^^^ ← iw (just "user")
^^^^^ ← aw (includes "-")
^^^^^^^^^^^^^^^^^^^← iW (entire "user-profile.avatar")
" hello world "
^
cursor
Before → after
Cursor on hello, press ciw, type hi, then press Esc:
Before: " hello world "
^
After: " hi world "
Result: only hello became hi — the quotes and spaces stayed put. daw would also delete the space after the word; diw leaves it.
Test each object in text-objects-practice.md (open it with the shell alias np-top):
| Command | Drill line to test | Expected result |
|---|---|---|
iw / aw | line 59 — " hello world " | iw grabs hello; aw grabs hello |
iw / aw / iW | line 60 — user-profile.avatar | iw=user, aw=user-, iW=whole WORD |
ciw / daw / yiW | line 61 — The quick brown fox... | change / delete / yank the target word |
Sentence and Paragraph
| Text Object | Selects |
|---|---|
| is | Inner sentence |
| as | Around sentence (including trailing space) |
| ip | Inner paragraph |
| ap | Around paragraph (including empty line) |
First sentence here. Second sentence follows. Third one closes it out.
^
cursor
Before → after
Cursor in the second sentence, press dis (delete inner sentence):
Before: First sentence here. Second sentence follows. Third one closes it out.
^
After: First sentence here. Third one closes it out.
Result: the whole second sentence is gone (with its trailing space); First sentence here. and Third one closes it out. are untouched.
Test in text-objects-practice.md (open it with the shell alias np-top):
| Command | Drill line to test | Expected result |
|---|---|---|
is / as / yas | line 156 — First sentence here. Second sentence... | select / yank one sentence |
dip / yap | lines 142-147 — the two paragraphs | delete / yank the whole paragraph |
Delimiter-Based Text Objects
These are the most productive text objects for code editing:
| Text Object | Selects inside... |
|---|---|
| i" a" | Double quotes "..." |
| i' a' | Single quotes '...' |
| i a | Backticks ... |
| i( a( | Parentheses (...) — also ib |
| i[ a[ | Square brackets [...] |
i{ a{ | Curly braces {...} — also iB |
i< a< | Angle brackets <...> |
| it at | HTML/XML tags <tag>...</tag> |
const name = "Alice Smith"
^
cursor
Before → after
Cursor inside "Alice Smith", press ci", type Bob Jones, then press Esc:
Before: const name = "Alice Smith"
^
After: const name = "Bob Jones"
Result: the quotes stayed; only the text between them changed.
Test each delimiter in text-objects-practice.md (open it with the shell alias np-top):
| Text object | Drill line to test | Expected result |
|---|---|---|
i" / a" / i' / a' / backticks | lines 71-73 — const name, greeting, cmd | change / yank inside the quotes |
i( / a( | line 33 — (hello world) | Drill 0 (see above) |
i[ / a[ | line 88 — const nums = [1, 2, 3, 4]; | yi[ yanks 1, 2, 3, 4 |
iB / aB (curly) | line 89 — const user = { name: "Bob", age: 30 }; | viB selects the body |
i< / a< | line 128 — <placeholder> | ci< leaves <> |
it / at | line 124 — <p>This is inside...</p> | dit keeps <p></p> |
Practical Code Examples
// function call: getData(userId, options)
// ^^^^^^^^^^^^^ di( → delete args
// ^^^^^^^^^^^^^^^ da( → delete args + parens
// string: const name = "Alice Smith"
// ^^^^^^^^^^^ ci" → change inner string
// ^^^^^^^^^^^^^ ca" → change including quotes
// array: const nums = [1, 2, 3, 4]
// ^^^^^^^^^^ yi[ → yank inner array
// ^^^^^^^^^^^^ da[ → delete including brackets
// object: const user = { name: "Bob", age: 30 }
// ^^^^^^^^^^^^^^^^^^^ viB → visual inner block
Test each snippet in text-objects-practice.md (open it with the shell alias np-top):
| Snippet | Drill line to test | Command |
|---|---|---|
getData(userId, options) | line 87 — const result = ... | di( / da( |
const name = "Alice Smith" | line 71 | ci" / ca" |
const nums = [1, 2, 3, 4] | line 88 | yi[ / da[ |
const user = { name: "Bob", age: 30 } | line 89 | viB |
Block Text Objects
| Text Object | Selects |
|---|---|
| i( / ib | Inner parentheses |
| a( / ab | Around parentheses |
i{ / iB | Inner curly braces |
a{ / aB | Around curly braces |
B (uppercase) specifically means curly braces — useful for navigating function bodies:
ciB → change entire function body (keep braces)
daB → delete function body including braces
const user = { name: "Bob", age: 30 }
^
cursor
Before → after
Cursor inside the braces, press ciB, type id: 1, then press Esc:
Before: const user = { name: "Bob", age: 30 }
^
After: const user = { id: 1 }
Result: the { } wrapper stayed; only the body between them was replaced.
Test in text-objects-practice.md (open it with the shell alias np-top):
| Command | Drill line to test | Expected result |
|---|---|---|
ciB / daB | line 89 — const user = { name: "Bob", age: 30 }; | change / delete the brace body |
diB / d2iB / >aB | line 105 — return getData(value); | innermost {} vs outer body vs indent |
Text Objects with Operators
| Combination | Action |
|---|---|
| ciw | Change inner word |
| daw | Delete around word |
| yi" | Yank inner double-quoted string |
| ca( | Change around parentheses |
| viB | Visual inner Brace block |
| dit | Delete inner HTML tag content |
| yap | Yank around paragraph |
| dip | Delete inner paragraph |
| cit | Change inner tag (edit HTML content) |
| >aB | Indent around Brace block |
Each combination is drilled in text-objects-practice.md (open it with the shell alias np-top):
ciw line 61 · daw line 61 · yi" line 71 · ca( line 33 · viB line 89 · dit line 124 · yap line 142 · dip line 142 · cit line 124 · >aB line 105
Text Objects Don't Require Precise Positioning
This is the key advantage — you can be anywhere inside the object:
"Hello World"
With cursor on 'l' in "Hello":
ci" → works! Changes "Hello World" to whatever you type
Before → after
Cursor on the l in Hello, press ci", type Goodbye, then press Esc:
Before: "Hello World"
^
After: "Goodbye"
Result: the whole "Hello World" became "Goodbye" — even though the cursor was only on the l, not at the start of the quotes.
Test in text-objects-practice.md (open it with the shell alias np-top):
| Command | Drill line to test | Expected result |
|---|---|---|
ci" | line 168 — "Hello World" | cursor on l in Hello still changes the whole string |
Nesting — Text Objects Find the Innermost Match
function outer() {
function inner() {
return getData(value); ← cursor here
}
}
diB → deletes "return getData(value);" (innermost {})
d2iB → deletes inner() function body (next outer {})
Before → after
Cursor on the return line, press diB:
Before:
function outer() {
function inner() {
return getData(value);
}
}
After:
function outer() {
function inner() {
}
}
Result: return getData(value); was removed — the innermost { } block is now empty. d2iB (count 2) removes the whole inner() body instead.
Test in text-objects-practice.md (open it with the shell alias np-top):
| Command | Drill line to test | Expected result |
|---|---|---|
diB / d2iB | line 105 — return getData(value); | deletes innermost {}; d2iB the outer function body |
Practice
Open the step-by-step drills: practice-folder/nvim/03-text-objects-and-motions/text-objects-practice.md
(also available via the shell alias np-top).
| Drill | What it trains | Steps |
|---|---|---|
| 1 | iw aw iW aW | On user-profile.avatar → iw (user) vs aw (user-) vs iW (whole WORD) |
| 2 | Quoted strings | On "Alice Smith" → ci" (keep quotes) vs ca' (drop quotes) |
| 3 | Brackets & braces | di( / da(; yi[ / da[; viB / ciB on the object literal |
| 4 | Nested blocks | Cursor on a nested return → diB (innermost) vs d2iB (outer) vs >aB (indent) |
| 5 | HTML tags | dit (keep tag) / cit (edit content) / vit / dat |
| 6 | Paragraphs | dip, yap, then paste with p |
| 7 | Sentences | is, as, dis, yas on a three-sentence line |
| 8 | Rapid-fire + mini.ai | Position-independent ci"; LazyVim cia to change one argument |
In LazyVim
Vanilla text objects (iw, ap, etc.) remain fully functional. However, LazyVim supercharges text objects by including mini.ai.
mini.ai extends the standard a and i text objects to cover a wider variety of surroundings (like quotes, brackets, and even function arguments or tags) without needing separate plugins for each.
| Aspect | Vanilla Neovim | LazyVim |
|---|---|---|
| Text Objects | Built-in objects (iw, as) | Enhanced via mini.ai |
| Function Arguments | Requires targets.vim or similar | Built into mini.ai (ia, aa) |
getData(userId, options)
^
cursor
Before → after
Cursor on the options argument, press cia (mini.ai), type flags, then press Esc:
Before: getData(userId, options)
^
After: getData(userId, flags)
Result: only options changed to flags; userId and the commas stayed. (This is a mini.ai extension — vanilla Vim has no argument object.)
Test the mini.ai extras in text-objects-practice.md (open it with the shell alias np-top):
| Command | Drill line to test | Expected result |
|---|---|---|
cia (mini.ai) | line 87 — getData(userId, options) | changes only the argument under the cursor |