
This is not a beginner’s guide. If you’ve used Vim daily for a year and still occasionally fight with paste behavior, you’re the target audience. This article covers the things I got wrong (or never properly learned) after fifteen years of daily Vim usage - and the moment everything clicked.
The focus is practical: editing configuration files, YAML, shell scripts, and infrastructure code. No Vim philosophy lectures. No “Vim is a language” metaphors. Just the patterns that make the biggest difference for sysadmins and DevOps engineers who spend their days in terminals.
Table of Contents
- Motions That Actually Matter
- Jumping, Not Crawling
- The Power of Text Objects
- Copy/Paste: The Thing You’ve Been Doing Wrong
- Search and Replace: Scoped to What You Need
- YAML: The Pain and the Antidote
- Registers and Macros: Automation for the Lazy
- Splits, Buffers, and Efficient File Navigation
- The Neovim Advantage: LSP and Treesitter
- Practical Combos: The Cheat Sheet
- Wrapping Up
Motions That Actually Matter
Everyone knows hjkl. Most people know w and b. Here’s what separates fast editing from “I’ll just use sed”:
Jumping, Not Crawling
| Motion | What it does |
|---|---|
f{char} / F{char} |
Jump to next/previous occurrence of {char} on the line |
t{char} / T{char} |
Jump to just before next/previous {char} |
; and , |
Repeat last f/t forward/backward |
% |
Jump to matching bracket/parenthesis |
{ / } |
Jump to previous/next empty line (paragraph) |
Ctrl-d / Ctrl-u |
Half-page down/up |
* / # |
Search word under cursor forward/backward |
gd |
Go to local definition of word under cursor |
The f/t family is criminally underused. Editing a firewall rule and need to change the port number? f: jumps to the colon, l moves one right, cw changes the word. Three keystrokes instead of mashing w twelve times.
The Power of Text Objects
This is where Vim stops being a text editor and starts being a scalpel. Text objects work with any operator (d, c, y, v):
| Object | Meaning | Example |
|---|---|---|
iw / aw |
inner word / a word (includes trailing space) | ciw - change word under cursor |
i" / a" |
inside quotes / around quotes | ci" - change contents of quoted string |
i( / a( |
inside parentheses / around parens | di( - delete contents between () |
i{ / a{ |
inside braces / around braces | ci{ - change block contents |
ip / ap |
inner paragraph / a paragraph | yip - yank entire paragraph |
it / at |
inside XML/HTML tag / around tag | cit - change tag contents |
Visualizing Text Objects
Text objects are easier to grasp with a concrete example. Given this code:
function_name() {
some_value = true
}
di{deletessome_value = true- everything between the braces, leaving{ }intactda{deletes{ some_value = true }- the braces AND their contents
Same logic applies to quotes: di" deletes just the string contents, da" deletes the quotes too. For paragraphs, dip deletes the text, dap also removes the trailing blank line.
Real-world example - you have a Jinja2 template variable {{ old_value }} and need to change it:
ci{ → deletes old_value, leaves you in insert mode between the braces
One combo instead of navigating, selecting, deleting, typing.
Copy/Paste: The Thing You’ve Been Doing Wrong
This was my fifteen-year blind spot.
The Problem
You visually select some lines with v, yank with y, move somewhere, hit p - and the text lands in the middle of a line instead of on a new line below. You undo, try P, and it ends up in the middle of the line above. Familiar?
Why It Happens
Vim tracks whether a yank was characterwise, linewise, or blockwise:
v(characterwise) →ycreates a characterwise register →pinserts inlineV(linewise) →ycreates a linewise register →pinserts below current lineCtrl-v(blockwise) →ycreates a blockwise register →pinserts as a column
The yank type determines the paste behavior. That’s it. That’s the whole mystery.
The Fix
Use V (Visual Line) when you want to copy/paste lines. This is the single biggest quality-of-life improvement:
V → enter visual line mode
jjj → select lines downward
y → yank (linewise)
} → jump to where you want it
p → paste below current line. Clean. New line. No surprises.
Quick Reference
| Want to… | Use |
|---|---|
| Yank current line | yy |
| Yank 5 lines | 5yy |
| Yank a paragraph | yip |
| Yank lines visually | V + move + y |
| Paste below | p (with linewise yank) |
| Paste above | P (with linewise yank) |
| Force paste as new line | :put (below) / :put! (above) |
The :put command is your safety net - it always pastes as a new line, regardless of how you yanked. Useful when you’ve already yanked characterwise and don’t want to redo it.
Named Registers: Your Clipboard Slots
Vim has 26 named registers ("a through "z). Use them:
"ayy → yank current line into register a
"bV3jy → yank 4 lines into register b
"ap → paste from register a
"bp → paste from register b
The system clipboard is register "+:
"+yy → yank line to system clipboard
"+p → paste from system clipboard
For Neovim, you can unify the clipboard by adding to your config:
vim.opt.clipboard = 'unnamedplus'
Now y and p use the system clipboard directly. Whether you want this is a matter of taste - I prefer keeping Vim’s registers separate and using "+ explicitly when I need the system clipboard.
Search and Replace: Scoped to What You Need
Global Replace
The classic everyone knows:
:%s/old/new/g
% means the entire file. g means all occurrences per line.
Replace Only in Visual Selection
Select a block with V (or v), then press :. Vim auto-fills the range:
:'<,'>s/old/new/g
This replaces only within the selected lines. But here’s the subtlety: with V (Visual Line), this replaces in the entire lines. If you selected with v (characterwise) and want to restrict the match to exactly the highlighted text:
:'<,'>s/\%Vold/new/g
The \%V atom constrains the match to the visual selection boundary, not just the lines it spans. Niche, but invaluable when you need surgical precision.
Useful Flags
| Flag | Effect |
|---|---|
g |
All occurrences per line (not just first) |
c |
Confirm each replacement |
i |
Case insensitive |
I |
Case sensitive (overrides ignorecase setting) |
n |
Count matches without replacing |
The c flag is underrated. :%s/foo/bar/gc lets you step through every match and decide with y/n. Much safer than blind replace on a production config.
Multi-File Replace with :argdo / :cfdo
Need to replace across multiple files? Open them and:
:args *.yaml
:argdo %s/old_value/new_value/g | update
Or use the quickfix list after a grep:
:vimgrep /pattern/ **/*.yaml
:cfdo %s/pattern/replacement/g | update
YAML: The Pain and the Antidote
YAML editing is where Vim either shines or makes you want to throw your keyboard. Here’s how to make it shine.
Indentation: The Basics
>> → indent current line one shiftwidth
<< → unindent current line
>ip → indent entire paragraph
5>> → indent 5 lines
V5j> → visual select 6 lines, indent
In Visual mode, > indents and then exits Visual mode. Use gv to reselect the same area, or better - just use . to repeat:
V5j> → select and indent
. → indent the same lines again (still selected by `'<,'>`)
Set Up Your YAML Defaults
Essential in your Neovim config:
vim.api.nvim_create_autocmd('FileType', {
pattern = 'yaml',
callback = function()
vim.opt_local.shiftwidth = 2
vim.opt_local.tabstop = 2
vim.opt_local.softtabstop = 2
vim.opt_local.expandtab = true
vim.opt_local.cursorcolumn = true -- vertical line at cursor column
end,
})
cursorcolumn draws a vertical highlight at your cursor’s column position - an instant visual guide for YAML indentation alignment.
Moving Blocks Up and Down
Rearranging YAML keys or Ansible tasks:
:m+1 → move current line down one
:m-2 → move current line up one
Or visually select a block and move it:
V3j → select 4 lines
:m '>+1 → move block down one line
:m '<-2 → move block up one line
Bind these for convenience:
-- Move lines up/down in visual mode
vim.keymap.set('v', 'J', ":m '>+1<CR>gv=gv")
vim.keymap.set('v', 'K', ":m '<-2<CR>gv=gv")
Now J/K in Visual mode moves the selected block and re-indents. Invaluable for reordering Ansible tasks.
Folding YAML Sections
vim.opt.foldmethod = 'indent' -- YAML's structure IS its indentation
vim.opt.foldlevelstart = 99 -- start with everything unfolded
Then:
| Key | Action |
|---|---|
za |
Toggle fold under cursor |
zM |
Close all folds |
zR |
Open all folds |
zc / zo |
Close / open one fold |
Folding by indent works perfectly for YAML because indentation is the structure. A folded Ansible playbook shows you just the task names - like a table of contents.
Registers and Macros: Automation for the Lazy
The Dot Command
The most powerful single key in Vim: . repeats the last change.
ciw"new_value"<Esc> → change word to "new_value"
n → jump to next search match
. → apply the same change
n.n.n. → repeat across all matches
This pattern - search, change, repeat - handles 90% of “I need to change this in five places” without reaching for :%s.
Quick Macros
Record with q{register}, replay with @{register}:
qa → start recording into register a
0f:w → jump to start of line, find colon, move to next word
ciw"<C-r>"" → change word, insert quote, paste deleted word, close quote
Esc → back to normal mode
j → move down
q → stop recording
5@a → replay 5 times
Note: ciw"<C-r>"" is the native way - ciw deletes the word, " starts the quoted string, <C-r>" (Ctrl-r followed by double-quote) pastes the deleted word from the default register, and the final " closes it.
For YAML quoting, this is also a great use case for vim-surround or mini.surround. With mini.surround, ysiw" wraps the word under cursor in quotes in a single command. No macro needed.
Replaying the Last Macro
@@ repeats the last played macro. Combined with a count: 20@@ - fire and forget.
Splits, Buffers, and Efficient File Navigation
Sysadmins often edit multiple related files: the playbook and the inventory, the nginx config and the upstream block, pf.conf and the anchor file.
Splits
:sp filename → horizontal split
:vsp filename → vertical split
Ctrl-w h/j/k/l → navigate between splits
Ctrl-w = → equalize split sizes
Ctrl-w _ → maximize current horizontal split
Ctrl-w | → maximize current vertical split
Ctrl-w o → close all other splits (:only)
Buffers
:e filename → open file in current buffer
:ls → list all open buffers
:bn / :bp → next / previous buffer
:b <partial> → switch to buffer by partial name match
:bd → close buffer
:b with tab completion and partial matching is surprisingly fast: :b pf<Tab> jumps to pf.conf if it’s open.
Jumping Between Recent Files
Ctrl-o → jump back through jump list
Ctrl-i → jump forward
`` → jump to last position before latest jump
`" → jump to position when last editing this file
Ctrl-o is the “undo for navigation.” Jumped somewhere with * or gd and want to go back? Ctrl-o. Multiple times if needed.
The Neovim Advantage: LSP and Treesitter
If you’re still on vanilla Vim, here’s what you’re missing. Neovim’s built-in LSP client and Treesitter integration transform YAML editing:
YAML Language Server
With yaml-language-server configured, you get:
- Schema validation - red squiggles when your Kubernetes manifest has a wrong field
- Auto-completion -
Ctrl-x Ctrl-osuggests valid keys for your schema - Hover docs -
Kshows documentation for the key under cursor
A minimal LSP setup in init.lua:
-- Requires nvim-lspconfig plugin
require('lspconfig').yamlls.setup({
settings = {
yaml = {
schemas = {
['https://json.schemastore.org/ansible-playbook'] = 'playbook*.yml',
['https://json.schemastore.org/github-workflow'] = '.github/workflows/*.yml',
},
},
},
})
Treesitter
Treesitter gives you syntax-aware text objects and highlighting:
require('nvim-treesitter.configs').setup({
ensure_installed = { 'yaml', 'bash', 'lua', 'json', 'toml', 'python' },
highlight = { enable = true },
indent = { enable = true },
})
With Treesitter, indentation and folding become structure-aware instead of relying on raw indent levels. It’s the difference between “this line has 4 spaces” and “this is a mapping value inside a sequence item.”
Practical Combos: The Cheat Sheet
A reference for the patterns that come up daily in sysadmin work:
| Situation | Keystrokes | What happens |
|---|---|---|
| Change a value in quotes | ci" |
Deletes contents between quotes, enters insert mode |
| Delete everything inside braces | di{ |
Clears the block |
| Yank a whole YAML section | yap or yip |
Direct paragraph yank (no visual mode needed) |
| Comment 10 lines | Ctrl-v 9j I# <Esc> |
Block insert # at start of 10 lines |
| Uncomment 10 lines | Ctrl-v 9j ll x |
Block select # (two columns) and delete |
| Replace in selection only | V select, :'<,'>s/a/b/g |
Scoped search-replace |
| Indent a block more | V select, > |
Shift right by shiftwidth |
| Sort lines | V select, :sort |
Alphabetical sort |
| Remove duplicate lines | V select, :sort u |
Sort and deduplicate |
| Reformat a long line | gq + motion (gqip for paragraph) |
Wrap to textwidth |
| Repeat last change everywhere | n.n.n. |
Search next, apply same edit |
| Save file you opened without sudo | :w !sudo tee % |
Writes via sudo without restarting the editor |
| Undo by time | :earlier 5m / :later 5m |
Jump to file state 5 minutes ago/ahead |
Wrapping Up
Vim rewards investment on a curve - the first week is brutal, the first year is productive, and the next decade is about discovering that you’ve been doing basic things inefficiently the entire time. The patterns in this article represent the subset that matters most for infrastructure work: efficient navigation, correct yank/paste semantics, scoped replacements, and YAML-specific workflows.
The best way to internalize these is not to memorize the table above but to deliberately use one new pattern each day until it becomes muscle memory. Start with V instead of v. Then graduate to ci". Then text objects. Each one compounds.
If you want a ready-made Neovim configuration optimized for Ansible, Python, and YAML work, check out nvim-ansible on Codeberg - it implements most of the patterns and plugins discussed in this article.
And if you’ve been using Vim for fifteen years and just learned that V solves your paste problems - you’re in good company.
Comments
You can use your Mastodon or other ActivityPub account to comment on this article by replying to the associated post.
Search for the copied link on your Mastodon instance to reply.
Loading comments...