Mastodon

Neovim Crash Course for Sysadmins: The 20% That Solve 80% of the Pain



Neovim Crash Course for Sysadmins

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

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{ deletes some_value = true - everything between the braces, leaving { } intact
  • da{ 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) → y creates a characterwise register → p inserts inline
  • V (linewise) → y creates a linewise register → p inserts below current line
  • Ctrl-v (blockwise) → y creates a blockwise register → p inserts 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 oclose all other splits (:only)

Buffers

:e filename     → open file in current buffer
:lslist all open buffers
:bn / :bp       → next / previous buffer
:b <partial>    → switch to buffer by partial name match
:bdclose 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-ojump back through jump list
Ctrl-ijump 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-o suggests valid keys for your schema
  • Hover docs - K shows 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...