Neovim from scratch / part 2
Rebuilding my Neovim config from scratch (Part 2: Keymaps)
Part 2 of rebuilding my Neovim config from zero: a beginner-friendly tour of Vim's modes, then the custom keymaps that make the editor feel like mine.
(Part 2 of me rebuilding my Neovim config from scratch on 0.12, no plugin manager, understanding every line. Part 1 was the options.)
Last time was settings. This time it’s the fun part: keymaps, teaching the editor my own shortcuts. But before any of that makes sense, I have to talk about the thing that confuses every single Vim beginner, including me, for an embarrassingly long time.
Modes. It’s all about modes.
If you’ve ever opened Vim, typed a sentence, and watched it gleefully execute half your words as commands and delete the rest, congrats, you’ve met modes the hard way. I did too.
Here’s the deal. The same key does different things depending on what mode you’re in:
- Normal mode (the default, and
Escalways brings you here). Keys are commands.jkhlmove around,ddeletes,xsnips a character. This is where you live most of the time, which sounds weird until it doesn’t. - Insert mode (press
i). Now it’s a normal text editor. Type, and letters appear. Shocking, I know. - Visual mode (press
v). You select text first, then do something to it.
That’s 90% of it. A keymap just says: “in this mode, when I press these keys, do this instead.” The Lua function is vim.keymap.set(mode, keys, action, opts), and the modes get one-letter names: "n", "i", "v".
A tiny helper so I don’t lose my mind
Writing out vim.keymap.set("n", ..., { silent = true, noremap = true }) for every single mapping is the kind of repetition that makes you quit a hobby. So, one little wrapper:
local function map(mode, lhs, rhs, desc)
vim.keymap.set(mode, lhs, rhs, { desc = desc, silent = true, noremap = true })
end
Now every mapping is just map(mode, keys, action, "what it does"). That desc at the end isn’t decoration: later in this series a plugin turns all those descriptions into a little pop-up cheat sheet, so writing them now is me doing future-me a favour.
The actual keymaps
My leader key is the spacebar (set back in Part 1). So <leader>w means “press Space, then w.” Here’s the file, in chunks.
Escape hatches. The two I reach for without thinking:
-- Press Esc in normal mode to clear leftover search highlighting
map("n", "<Esc>", "<cmd>nohlsearch<CR>", "Clear search highlight")
-- Type "jk" in insert mode to pop back to normal mode,
-- so my pinky never has to go hunting for the real Esc key
map("i", "jk", "<Esc>", "Exit insert mode")
That jk one is a little controversial and I’ll be honest about it below. But my hands love it.
Save and quit. Remembering :w and :q is fine until it isn’t:
map("n", "<leader>w", "<cmd>write<CR>", "Save file")
map("n", "<leader>q", "<cmd>quit<CR>", "Quit window")
map("n", "<leader>Q", "<cmd>quitall<CR>", "Quit everything")
Splits. Two files side by side. The s is for “split”:
map("n", "<leader>sv", "<cmd>vsplit<CR>", "Split left|right")
map("n", "<leader>sh", "<cmd>split<CR>", "Split top/bottom")
map("n", "<leader>se", "<C-w>=", "Make splits equal size")
map("n", "<leader>sx", "<cmd>close<CR>", "Close this split")
…and to hop between those splits, Ctrl plus a direction. h/j/k/l are already Vim’s left/down/up/right, so this reuses muscle memory I’m building anyway:
map("n", "<C-h>", "<C-w>h", "Go to split on the left")
map("n", "<C-j>", "<C-w>j", "Go to split below")
map("n", "<C-k>", "<C-w>k", "Go to split above")
map("n", "<C-l>", "<C-w>l", "Go to split on the right")
Comfort stuff. Keep my eyeballs centred:
-- Half-page jumps that re-center instead of flinging me to a screen edge
map("n", "<C-d>", "<C-d>zz", "Half-page down, centered")
map("n", "<C-u>", "<C-u>zz", "Half-page up, centered")
-- Center the next/previous search match too
map("n", "n", "nzzzv", "Next match, centered")
map("n", "N", "Nzzzv", "Prev match, centered")
The zz means “scroll so the current line sits in the middle.” Once your jumps stop dumping you at the very bottom of the screen, you stop losing your place. Cheap trick, big quality-of-life.
Visual mode niceties:
-- Indent a selection and KEEP it selected, so I can indent again and again
map("v", "<", "<gv", "Indent left, keep selection")
map("v", ">", ">gv", "Indent right, keep selection")
-- Move the selected line(s) up/down and auto-reindent them
map("v", "J", ":m '>+1<CR>gv=gv", "Move selection down")
map("v", "K", ":m '<-2<CR>gv=gv", "Move selection up")
That last pair, selecting a few lines and sliding them around with J/K, feels like cheating the first time you do it. Deeply satisfying.
A note on <cmd> (because it tripped me up)
You’ll notice I write <cmd>write<CR> instead of :write<CR>. They look the same but <cmd> runs the command directly: it doesn’t visibly drop you into the : command line or fuss with your current mode. It’s the modern, no-glitches way to hang a command off a key. Just use it and move on.
The two I’m side-eyeing
I flagged these in my config as “optional flavour,” because they override default behaviour and not everyone will like them:
jk→ Escape. If you ever type a real “jk” in prose, it pauses for a beat (thattimeoutlenfrom Part 1) before letting it through. Mildly annoying in theory. I’ve never hit it in practice.- Visual
J/K. NormallyJjoins lines. I gave that key to “move selection” instead, because I move lines far more often than I join them. Your mileage may vary. It’s two lines to delete if you disagree.
That’s the whole point of building this myself, though: when I disagree with a default, I just… change it. The editor works for me, not the other way around.
Try it
Open two files, press <leader>sv to split, bounce between them with <C-h> and <C-l>, drop into insert with i, type some nonsense, jk back out, <leader>w to save, <leader>sx to close. That little loop is most of my day, and it already feels like mine.
Next time we finally leave the built-in-only world and install our first plugin, using Neovim 0.12’s brand-new vim.pack, the built-in plugin manager almost nobody’s written about yet. We’re going to make the editor pretty and learn exactly how a plugin goes from “a link on GitHub” to “running in my editor,” with zero magic in between.
Cheers!
Comments
Comments require a GitHub account. No GitHub? Reply on Substack or email me.