chan.dev / posts

Change

🌱 This post is in the growth phase. It may still be useful as it grows up.

Vim is hard to learn.
Additionally so because people suck at teaching Vim.

So if you’ve wanted to learn, but haven’t yet, it’s not really your fault 😁.

I propose we learn a little differently.
Instead of starting at the beginning (with the basics),
Let’s start at the end — with Vim’s most powerful macros.

I figure…
🥉 Worst case: you learn nothing.
🥈 Second-worst case: you learn one of Vim’s best capabilities (and nothing else). 🥇 Best case: you master a few commands and get hyped for more more.

Obviously I think you and I can get to that bast case scenario or I wouldn’t have written this guide.

Wanna learn some Vim?

Contents

Intent: the power of Vim

Vim is the most powerful editing tool I’ve ever used.

And it’s powerful because of a simple idea: editing intent.

Think about how much time you spend making text selections with a mouse or arrow keys.
Changing text inside quotes, delete extranious adjectives, moving paragraphs…
You make a text selection, then do something with it.

This is different in Vim.

If we want to change the text in-between quotes, we don’t need have to make a visual selection first.
We communicate our full intent through commands and macros.
ci does exactly what we want with just four characters.

This guide covers 100 such operations in this guide with practical, real-world editing examples.

How to read this guide

Lessons in this guide include 3 parts:

  1. A description of the editing intent
  2. An table of Vim operations that fulfull that intent
  3. A few considerations, shortcomings, and exceptions to consider

We start with the base operation ci (change inner).
This is my favorite editing intent, and Vim’s most capable.

Change “inner” is the termonoligy you’ll find in the docs.
But I use “inside” and “in-between” where it sounds better to say.

Finally, every one of these commands is run from Normal mode.
In normal mode, you’ll type each character in the operation.
Entry speed should not matter in stock Vim/neovim.


Change inside quotes

Change text inside the current (or next) pair of quotes.

OperationDescription
cichange inside single quotes
cichange inside double quotes
ci`change inside backticks

Note: The cursor does not need to be inside the target quotation to be changed. But it does need to be before the quotation.


Change inside brackets, braces, and parentheses

Change text inside the current (or next) pair of braces, brackets, and parentheses.

operationAlternativeAliasDescription
ci(ci)cibchange inside parentheses
ci{ci}ciBchange inside curly braces
ci[ci]change inside square brackets
ci<ci>change inside angle brackets

Both opening and closing characters target the same selection.
But I recommend committing to the opening character.
Here’s why:

  1. Committing to an operation creates stronger muscle memory.
  2. Minimal, columnar-style keyboards often remove physical representation of closing characters. So choosing the right side could require additional keystrokes, if a columnar-style keyboard is in your future.

Note: Parentheses and curly braces have aliases that are easier to type.


Change inner word, sentence, or paragraph

Change the word, sentence, or paragraph under the cursor. This works indifferent to the cursor’s position inside that selection.

OperationDescription
ciwchange inner word
ciWchange inner WORD (includes punctuation)
cischange inner sentence (determined by punctuation)
cipchange inner paragraph (determined by whitespace)

Repeat operations

It can be useful to repeat an operation.

Prefixing an operation with a number will execute that operation the number of times spcefied. This number is referred to as an operation’s count.

These are the operations from the previous lesson with various counts.

operationDescription
2ciwchange inner word under cursor and the next
3ciWchange inner WORD under cursor and next 2 WORDs (includes punctuation)
3cischange inner sentence under cursor and the next
2cipchange inner paragraph under cursor and the next

Change around

OK. Let’s talk about text selection composition now.

Vim operations are composable.
For the curious, the existance of an inner distinction suggests an “outer”.

And Vim delivers.

This distinction is around (think “including”).

  • “Change around (including) double quotes.”
  • “Change around sentence (including whitespace and punctuation).”
  • “Change around block (including curly braces).”

For every inner operation we’ve learned, there is a corresponding around operation, which includes characters and/or whitespace.

This table is every ci command we’ve learned, but using the ca (around) variation.

OperationAlternativeAliasDescription
cachange around (including) single quotes
cachange around (including) double quotes
ca`change around (including) backticks
ca(ci)cibchange around (including) parentheses
ca{ci}ciBchange around curly braces
ca[ci]change around (including) square brackets
ca<ci>change around (including) angle brackets
cawchange arround word (including whitespace)
caWchange arround WORD (including punctuation and whitespace)
caschange around sentence (determined by punctuation. including whitespace)
capchange arround paragraph (determined by whitespace. including whitespace)

What is a text object?

Text objects are an important concept in Vim.
And one you now have experience with.

i" (inside double quotes) is a text object.
ab (around parentheses) is a text object.
iW (inner WORD) is a text object.

Think of these as queries &mdash or text selections.
I’ve referred to them as “selections” up to this point.

These text objects are composable.
They can be used with commands other than c (change).

Let’s learn how…


Delete, don’t change

Let’s talk more command composition.

Our base c (change) command is actually two commands in one: d (delete) followed by i (insert).
And anything composed can be decomposed.

So what happens if we swap the decomposed d command, instead of the (composed) c command?

We get a new set of operations that delete text objects and keep us in Normal mode.
This is helpful when text objects need only be deleted (not replaced).

Let’s look at the board to see every c operation, as a d operation.

OperationAlternativeAliasDescription
dadelete around (including) single quotes
dadelete around (including) double quotes
da`delete around (including) backticks
da(ci)cibdelete around (including) parentheses
da{ci}ciBdelete around (including) curly braces
da[ci]delete around (including) square brackets
da<ci>delete around (including) angle brackets
dawdelete around word (including whitespace)
daWdelete around WORD (including punctuation and whitespace)
dasdelete around sentence (determined by punctuation. including whitespace)
dapdelete around paragraph (determined by whitespace. including whitespace)
didelete inner single quotes
didelete inner double quotes
di`delete inner backticks
di(ci)cibdelete inner parentheses
di{ci}ciBdelete inner curly braces
di[ci]delete inner square brackets
di<ci>delete inner angle brackets
diwdelete inner word (including whitespace)
diWdelete inner WORD (including punctuation and whitespace)
disdelete inner sentence (determined by punctuation. including whitespace)
dipdelete inner paragraph (determined by whitespace. including whitespace)

Count revisited

Now that you know know about commands and text objects,
There are things you need to consider when using counts.

Applying a count runs the command in sequence &mdash as if you typed it multiple, consecutive times.

Consider 3ci” (3x, change inner double quotes).

  1. Delete existing text in the double quotes and places the insert cursor inside the quotes.
  2. (Now inside quotes) perform the the same task — resulting in no descernable change.
  3. (Still inside quotes) perform the the same task — resulting in no descernable change.

This is even more confusing when manipulating words, sentences, and paragraphs.
Consider 3cis (3 times change inner word).

  1. Delete the sentence under the cursor, ignoring whitespace.
  2. (Now over the remaining whatespace) perform the same task — deleting that remaining whitespace.
  3. (Now with a new sentence under the cursor) perform the same task — deleting the next sentence (also ignoring whitespace).

This effect repeats, feeling like an off-by-one error.

For these reasons, repeating over inner operations is largely flawed.
So I favor around for when performing counted operations.

Note: See plugins for the ability to add a count to text objects.


The composability of operations

We now have 44 discrete operations, which can be repeated indefinitely. That’s a lot.

So how can we think about them composably?
Let’s start with anatomy.

(<count>) <command> <text object>

Let’s break down a few as examples.

ciw

segmentvaluedescription
countnone(default: 1x)
commandcchange
text objectiwinner word

2cib

segmentvaluedescription
count22x
commandcchange
text objectibinner parentheses

3dap

segmentvaluedescription
count33x
commandddelete
text objectaparound paragraph

Every operation you’ve learned can be decomposed into these three segments.


Use plugins for extended functionality

The mini.nvim/ai plugin includes additional selection tools and aliases.

Check out their documentation for additional features.
Here are are a few operations that I use often.

OperationDescription
ci*Change inside (arbitrary) * (useful in Markdown)
ci_Change inside (arbitrary) _ (useful in Markdown, or renaming snake_case variables)
ci/Change inside (arbitrary) / (useful with Regex)
ci?Change 2 <interactive: prompt for left and right edges>
ciqChange inside quotes (', ", `)
cinqChange inside next quotes
cilqChange inside last quotes
cifChange inside function call
ciaChange inside argument
c2iaChange inside 2nd argument
d2aaDelete 2nd argument in function call/definition
c2itChange 2 inner tags (atomic selection)

Take selection farther with motions

We did not cover motions.

Motions are are not text objects.
They are Vim’s way of moving the cursor.
However, like text objects, they can be used with commands to perform operations.

This is a comprehensive (but not exhaustive) list of motions composed with the c command.

commandalternativealiasdescription
cbchange to beginning of the previous word
cBchange to beginning of the previous WORD (includes punctuation and whitespace)
cechange to end of the word (includes punctuation and whitespace)
cEchange to end of the WORD (includes punctuation and whitespace)
cf <character>change through next (find) <charecter>
cF <character>change through previous (find) <charecter>
cggchange to start of file
cGchange to end of file
ckc⬆︎change current and previous line
cjc⬇︎change current and next line
clc➡︎schange character under cursor
chc⬅︎change character before cursor
cHchange to top of screen (excluding padding)
cMchange to the middle of the screen
cLchange to bottom of screen (excluding padding)
cnchange to next search result
cNchange to previous search result
cpchange to previous search result
cPchange to previous search result
ct <character>change to next <charecter>
cT <character>change to previous <charecter>
cwchange to start of next word
cWchange to start of next WORD (includes punctuation and whitespace)
c0change (from cursor) to start of line
c$Cchange (from cursor) to the end of the line
c#change to previous word under cursor (word, if none found)
c*change to next word under cursor (word, if none found)
c%change to matching character pair (forward or backward)
c^change to first non-whitespace character on the current line
c)change to the beginning of the current sentence
c)change to the end of the current sentence
c{change to the beginning of the current paragraph
c}change to the end of the current paragraph
c-change to the first non-whitespace character on the previous line
c+change to the first non-whitespace character on the next line

Note: a few omissions: [[, [], ]], ][, etc. :help object-motions.txt

These are additional text object text selections.

commandaliasdescription
clschange character
cgnchange the next search pattern match
cg_change from cursor to the last non-blank character of the line
ccchange current line
c$Cchange from the cursor to the end of the line

There are no interactions with the g command prefix. Test.


Debug text object selections with Visual mode

You may feel uneasy about text objects.
This is natural as we’re used to visualizing our selections.

This is where Vim’s Visual mode comes into play.

We can replace the c and d commands with v to first perform a visual selection.
Then, with that selection, post-fix the c or d command.

Here are a few examples using Visual mode with text objects and motions:

operationdescription
viwcvisual select inner word and change
vapdvisual select around paragraph and delete
vicvisual select inside double quotes and change
vt*dvisual select to * and delete
vT-cvisual select backward to - and delete

Note: Count is not supported in Visual mode. This makes sense when you consider that count repeats commands, it does not compound them. Instead prefixing v with a number will select 11 × characters.


Common workflows

HTML/XML

operationdescription
citchange inside tag
cichange inside double quotes
ctchange to double quote (to beginning of string attribute)
cTchange back-to doble quote (to beginning of string attribute)
ci<change tag name
ctchange to space (handy for changing individual class names and other string attributes)
cTchange back-to space (handy for changing individual class names and other string attributes)
ciwchange string attribute segment (separated by non-word characters, e.g., -, _, ., etc.)

Note: Remember that that these operations can use d (delete) and v (visual) commands and/or a (arround) text objects selections.

Additional commands with mini.nvim/ai

operationdescription
c<count>itchange inside (ansestor) tags
cilqchange inside last quotes (universal)
c<count>inqchange inside next <count> quotes (universal)
c<count>inBchange inside next <count> curly brarces
sample.html
<html>
<head>
<title>title</title>
</head>
<body>
<main>
<h1 class="class-one-a class-two" id="my-id">Page title</h1>
</main>
</body>
</html>

React

All operations from HTML section apply.

operationdescription
cibchange inside parentheses
ciBchange inside curly braces
ca{change attribute in quotes and insert { to interpolate variable.

Additional commands with mini.nvim/ai and [mini.nvim/surround][mini/surround]

operationdescription
cifchange function call arguments
ciachange argument under cursor (or next)
<count>ciachange argument at <count> position
sr`surround replace " with ` to allow interpolation
<count>diachange argument at position <count>
daa(ripple) delete first argument
d<count>aadelete argument at position <count>
c<count>inBchange inside next <count> curly brarces
srq?replace surrounding quotes (universal) with <interactive> ({`, `}) (for changing string to variable interpolation)
function(a,b,c) {
return a + b + c;
}
function({a,b = 1, ...c}) {
return a + b + c;
}
export default function (a, b, c) {
return (<html>
<head>
<title>title</title>
</head>
<body>
<main>
<h1 class="class-one-a class-two" id="my-id">Page title</h1>
</main>
</body>
</html>)
}

Markdown/prose

[[: go to previous heading ]]: go to next heading ci*/ci_: change inside em/strong ci*/ci_: change inside em/strong cis/cas: change sentance cip/cap: change paragraph