Vim grammar: verb + noun
Vim has its own grammar. When you grasp it, it's like a paradigm shift: you can run Vim commands intuitively.
There is only one grammar rule in the Vim language:
verb + noun
That's all you need to know. It's like saying:
- "Eat (verb) a pancake (noun)"
- "Kick (verb) a soccer ball (noun)"
- "Learn (verb) Vim editor (noun)"
Vim verbs are operators
Vim has 16 operators (:h operator). Some examples:
y Yank text (copy)
d Delete text and save to register
c Delete text, save to register, and start insert mode
Vim nouns are motions
Vim nouns are motions. Motions can be used independently; they're used to move around in Vim. Some examples:
h Left
j Down
k Up
l Right
w Move forward to the beginning of the next word
} Jump to the next paragraph
$ Go to the end of the line
Vim nouns are also text objects
Vim nouns are not just motions. There are also text objects. Text objects must be used with verbs (operators). If it's your first time using Vim, it may sound weird, but trust me, when you grasp it, it's intuitive!
There are 2 kinds of text objects: inner and outer.
i + object Inner text object
a + object Outer text object
Don't get intimidated by them. Learn more at :h text-objects.
Combining verbs and nouns
You don't have to memorize everything before getting started. You can memorize 20% of the nouns and verbs and get really productive (I personally use y, d, and c about 80% of the time; there are even some operators that I haven't used in years).
learn = 'vim'
If your cursor is on l, you can delete everything from the current location to the end of the line with d$: verb (delete d) + noun (end of the line $). If your cursor is on learn and you want to delete the whole word, use diw: verb (delete d) + noun (inner word iw).
Vim also has a "convention" where pressing the operator twice means "apply this operator on the whole line". So dd deletes the whole learn = 'vim' line.
I wrote more on Vim grammar in Learn Vim, chapter 4. if you want to learn more.
Why Vim grammar is intuitive
Practice using Vim grammar multiple times. Start with one operator (I suggest d), then expand to other operators. Once you get used to it, it's an awesome feeling: you can learn about a new operator and immediately know how to use it. Let's say you learn about gu (the lowercase operator). You want to lowercase the current word. What do you press?
Lowercase + the current word.
gu (lowercase) + iw (the inner word)
So you press guiw ... and hey!! You just lowercased the inner word! Neat!
Next, you want to lowercase everything to the end of the line.
Lowercase + to the end of the line.
gu (lowercase) + $ (from the current location to the end of the line)
You press gu$.
Vim has a convention - by repeating the operator key twice, it acts on the whole line. For example, dd deletes the current line, yy yanks the current line. But d is a one-letter operator and gu is a two-letter operator. What do you do? gugu or guu?
Doesn't matter. Both gugu and guu work!
The best part is, you just knew, instinctively, that it would work. Learn a new operator and you know it will work with motions and text objects. Learn a new motion or text object and you know it will work with every operator. All you need to add to your muscle memory is the new piece.
Vim grammar can be expanded
Society keeps adding new words to our vocabulary. Some examples:
- "Dang it, that seagull photobombed our family photo"
- "That dude drove a new Tesla just to flex on his coworkers"
- "We're just gonna vibe on the patio this Memorial Day weekend and chill"
In the past 10-20 years we added many new words that I'm probably not aware of because I'm old and not cool. The point I'm making is not that I'm old and not cool (although that's sadly true), but that Vim grammar, too, can be expanded! Just like our vocabulary, Vim grammar is not fixed. We can add new Vim verbs.
One way is to install plugins. There are plugins that add operators, motions, and text objects, like vim-commentary, vim-sandwich, and vim-exchange.
Another way is to create your own operator - which is what this article is about.
How to create your own Vim operator
A custom Vim operator is a mapping that sets 'operatorfunc' to your function and returns g@.
I learn better from examples, so let's jump straight to one. Let's create a new operator, gz, that reverses text. Pass it 'hello' and you get 'olleh'.
Here's the full code:
function! Reverse(type) abort
if a:type ==# 'line'
normal! '[V']"ry
elseif a:type ==# 'block'
execute "normal! `[\<C-V>`]\"ry"
else
normal! `[v`]"ry
endif
let @r = join(reverse(split(substitute(@r, '\n$', '', ''), '\zs')), '')
normal! gv"rp
endfunction
function! s:SetupReverse() abort
set operatorfunc=Reverse
return 'g@'
endfunction
nnoremap <expr> gz <SID>SetupReverse()
xnoremap <expr> gz <SID>SetupReverse()
nnoremap <expr> gzz <SID>SetupReverse() .. '_'
Spoiler alert: the magic ingredient to create a new operator is g@ (:h g@). I'll go over this in just a little bit. The code above is composed of 3 parts:
- The transformer function (
Reverse) - The operator function (
SetupReverse) - The key mappings (the
noremapexpressions)
Let's start with the last part, the mappings.
The key mappings
nnoremap <expr> gz <SID>SetupReverse()
xnoremap <expr> gz <SID>SetupReverse()
nnoremap <expr> gzz <SID>SetupReverse() .. '_'
nnoremap maps gz to SetupReverse in normal mode. This allows you to press gz + motion/text object like any operator. xnoremap maps gz to the same function in visual mode (v, V, <C-V>). This lets you to first highlight a text then press gz to reverse them. This is also how any native Vim operator behaves.
The last nnoremap maps gzz to SetupReverse concatenated with _. Why _?
Recall Vim has the convention that doubling an operator key makes it act on the current line: dd, yy, cc, >>, gUU, guu. To follow that convention, our operator needs one extra mapping. _ is actually a motion and it means "this line, linewise" (:h _). You technically don't need the mapping because gz_ already works, just like d_ is the same as dd and gu_ the same as guu - but it's nice to follow Vim convention. My muscle memory is itching to press gzz to reverse the whole line.
By the way, if you're one of those people who actually prefer gugu instead of guu and gUgU instead of gUU, feel free to add a map for gzgz:
nnoremap <expr> gzgz <SID>SetupReverse() .. '_'.
<SID> means script-local. I won't go over it in detail here (:h <SID>); all you need to know is that SetupReverse is a script-local function (defined with s:), and from a mapping we can't call SetupReverse() directly, we need <SID>SetupReverse(). Reverse, on the other hand, is global, because Vim calls operatorfunc (:h 'operatorfunc') from outside our script.
The operator function: set operatorfunc, return g@
Ok, so we use 3 mappings to create our operator, but how does that work? I thought nnoremap was for "basic" mappings, like:
nnoremap <Leader>vs :source $HOME/.vim/vimrc<CR>
I press <Leader>vs and Vim sources my vimrc. That's it. I don't pass it a motion or a text object. I don't press <Leader>vs and then $. I didn't know I could press a motion / text object after my mapping? Yet somehow this mapping lets me pass motions and text objects, like gz$ or gziw, and makes gz act like an operator:
nnoremap <expr> gz <SID>SetupReverse()
You're actually right. There is nothing magical about the mapping. <expr> means "evaluate this expression and type whatever it returns". SetupReverse() returns 'g@', so pressing gz is the same as typing g@.
The secret sauce is in what g@ does, not in the mapping. Let's look inside SetupReverse(). It does 2 things: sets operatorfunc with a function (our Reverse function) and returns g@.
function! s:SetupReverse() abort
set operatorfunc=Reverse
return 'g@'
endfunction
What g@ does in Vim
g@ is a built-in operator that waits for a motion. You type a motion or text object (iw), and Vim runs whatever function is assigned to operatorfunc on that region.
What happens when you press gziw:
gzcallsSetupReverse()SetupReverse()setsoperatorfunctoReverse- It returns
g@, which Vim types for you g@enters operator-pending mode: it waits for a motion or text object- You press
iw(hey, a text object!) g@callsoperatorfunc(Reverse) and passes it a type (more on this below)
Take a pause and digest that. This is where the magic happens!
There is nothing magical about nnoremap. When g@ gets one, Vim says, "Hey, here's a region. Now run operatorfunc on it". This is how you make an operator in Vim.
The transformer function
Let's see the function assigned to operatorfunc. It looks complicated. Let's break it down!
function! Reverse(type) abort
if a:type ==# 'line'
normal! '[V']"ry
elseif a:type ==# 'block'
execute "normal! `[\<C-V>`]\"ry"
else
normal! `[v`]"ry
endif
let @r = join(reverse(split(substitute(@r, '\n$', '', ''), '\zs')), '')
normal! gv"rp
endfunction
Pay attention to what Reverse()'s argument is not. It's not text. I'd expect gziw on "class" to mean "reverse the word 'class' I'm passing you", and gzz on "learn = 'vim'" to mean "reverse this line, pls". But Reverse doesn't receive text. It receives a type. What the heck is type? And how does it know which text to transform? Where's the text??
The three region types: char, line, and block
In Vim there are 3 region types: 'char', 'line', and 'block'. Vim picks one based on how the region was produced, i.e. which motion or text object you passed. Every motion in :h motion.txt is either characterwise or linewise:
| Type | Motions |
| ------ | ---------------------------------------------------------------------------------------------------------- |
| char | w, b, e, $, 0, ^, h, l, f/t, %, `mark, iw, aw, i", i(, it, /pattern … |
| line | j, k, _, G, gg, 'mark, ip, ap, +, -, H/L/M … |
In general, motions that move up/down or cover whole lines are linewise; anything that lands on a specific column is characterwise. You can force a type with the visual mode keys: gzvj forces j to be characterwise, gzV$ forces $ to be linewise, gz<C-V>iw forces blockwise (:h o_v).
The 3 types mirror the 3 visual modes:
| Visual mode | Type |
| ----------- | ------- |
| v | char |
| V | line |
| <C-V> | block |
Want to see it yourself? Add an echo inside Reverse:
function! Reverse(type) abort
echomsg a:type
...
You don't have to memorize which type each motion translates to. Just know that gziw → char, gzj → line, <C-V>jjgz → block, and gzz (AKA gz_) is always line because _ is a linewise motion.
Wait, so Reverse only gets the type, not the text? How does it know what text to transform?
Yup. We have to go through some hoops to get the text. But type is what we need to do the job.
Reselecting the region with '[ and ']
Using this text again:
learn = 'vim'
Let's say you did gzz (or equivalently, gz_). That's a line type.
if a:type ==# 'line'
normal! '[V']"ry
Vim runs normal! '[V']"ry. What the heck is that?
normal! runs normal-mode keys (the ! ignores your mappings). '[ jumps to the first line of the text you just operated on (:h '[). V starts linewise visual mode. '] jumps to the last line of the text you just operated on (:h ']). Stop right there. '[V'] re-highlights the entire region you just operated on (learn = 'vim', in linewise visual mode). That's it.
Try it yourself. Find a random line in your file and run a line operation on it (gUU, gU2j, y10j). Then press '[ (or its backtick variation, `[, which goes to the exact character). Where is your cursor? Now press V, then ']. Where does that take you? Vim just re-selected the whole text you operated on!
Now the final part: "ry. "r means "whatever I'm about to do, use register r", and y yanks. We just yanked the region into register r (any register works; nothing is special about r).
Next:
let @r = join(reverse(split(substitute(@r, '\n$', '', ''), '\zs')), '')
This is the transformation. @r is the content of register r. A linewise yank ends in a newline, so substitute(@r, '\n$', '', '') strips it first (otherwise the newline would get reversed to the front, and you'd paste an empty line above your text). split(..., '\zs') splits the string into characters (\zs is a zero-width match, so it works on texts), reverse() reverses the list, and join() puts it back into a string. The result is assigned to @r. Register r now holds the reversed text.
Finally:
normal! gv"rp
gv re-selects the previous visual area in the same mode. Since we just did '[V'], that's the line learn = 'vim'. "rp pastes register r over it. Pasting over a visual selection replaces it.
And that, my friend, is how you create your own operator in Vim.
What about the other 2 branches?
elseif a:type ==# 'block'
execute "normal! `[\<C-V>`]\"ry"
else
normal! `[v`]"ry
Same concept. The char branch re-highlights with `[ and `] (exact characters, not whole lines) in v mode and the block branch does it in <C-V> mode. The latter is rare (usually invoked with <C-V> first), and our one-line transformation doesn't know about block shapes, so it will scramble a block. Making it block-aware is a good exercise (hint: getreginfo() and setreg() let you transform the register line by line while keeping its type).
The bare minimum: g@ and operatorfunc
You technically don't even need nnoremap and SetupReverse for the operator to work because g@ does most of the work for you. Try this. Assign operatorfunc to the transformer function:
:set operatorfunc=Reverse
Now you are ready to invoke the Reverse function. Press g@iw, it will reverse the inner word. Press g@$, it will reverse the texts to the end of the line. Press g@_, it will reverse the current line. g@ invokes whatever function is assigned to operatorfunc. That's really all you need!
Summary: how a custom Vim operator works
To create a Vim operator, pick a key (here, gz) and map it to a function that does 2 things:
- Set
operatorfuncto the transformer function - Return
g@
Creating an operator in Vim is no magic. It's just a bunch of Vim operations encapsulated in functions.
g@ goes into operator-pending mode and waits for a motion or text object. It then calls the transformer with the region's type: char, line, or block. Depending on the type, the transformer re-selects the region in the matching visual mode, yanks it, transforms it, and pastes it back.
The code I showed here works, but it is still pretty rough. It 'pollutes' your registers: whatever you had in register r is now replaced with the operated-on text, and the visual p overwrites the unnamed register too. It also pollutes your marks and jumps: '<, '> (what gv uses) and the jumplist now point at whatever we just highlighted. A good operator saves those states in temporary variables, does its transformation, then restores them so Vim "forgets" the operation ever happened.
I wrote a plugin, vim-operatorify that lets you to create an operator easily. Check out my plugin to see how to handle some rough edges. Pay attention to how I created temp variables (save) and restored them after the transformation was done in the finally block.
What makes a custom Vim operator feel native
A good operator should feel native.
- It uses Vim's own API:
g@and'operatorfunc', not a reimplementation of operator semantics. - It composes with the full motion grammar, including text objects from plugins you install next year.
- Follows the double-letter-convention for linewise effect.
gzzis implemented asg@_, just likedd,yy,>>,guu. - Works on visual mode too.
- Dot-repeat works.
g@gives you.for free. - Counts work.
gz3jreverses four lines. - Vim's conventional home for extra operator is to use
g*(make suregzisn't taken; check with:h map-which-keys).
What's next?
Now that you know how to make an operator, create your own!
Happy Vimming!