I left VS Code about a year ago. Not because it was bad — it's a fine editor — but because I wanted to understand my tools at a deeper level, and because the mouse was slowing me down. Neovim, combined with tmux, gave me an environment where my hands never leave the keyboard and every piece of the setup is something I chose, configured, and understand. This post is a complete walkthrough of that setup: what I run, how it's organized, and why each piece is there.
Everything here is in Lua. The config is modular, managed by lazy.nvim, and lives in a Git repo so I can clone it onto any machine and be productive in minutes. If you're thinking about making the switch, or just want to steal some ideas, this is for you.
Directory Structure
The whole config lives at ~/.config/nvim and follows a clean module pattern. The entry point is init.lua, which loads two things: core settings and the plugin manager.
Each plugin gets its own file. lazy.nvim automatically picks up everything in the plugins/ directory. To add a new plugin, you just drop a new Lua file in there. To remove one, delete the file. Clean separation, no giant monolithic config.
Entry Point
The entire bootstrap is two lines:
require("jaken.core")
require("jaken.lazy")
That's it. Core settings load first (so options like mapleader are set before any plugins reference them), then lazy.nvim bootstraps and loads all plugins.
Options
These are the editor-wide settings that apply regardless of what plugins are loaded. The important choices:
local opt = vim.opt
-- Line numbers: relative + absolute on current line
opt.relativenumber = true
opt.number = true
-- 2-space indentation, spaces not tabs
opt.tabstop = 2
opt.shiftwidth = 2
opt.expandtab = true
opt.autoindent = true
opt.wrap = false -- no line wrapping
opt.cursorline = true -- highlight current line
opt.termguicolors = true -- 24-bit color
opt.background = "dark"
opt.signcolumn = "yes" -- always show sign column
-- Search: case-insensitive unless you type uppercase
opt.ignorecase = true
opt.smartcase = true
-- System clipboard integration
opt.clipboard:append("unnamedplus")
-- Splits open to the right and below
opt.splitright = true
opt.splitbelow = true
Relative line numbers are essential for Vim motions. Instead of counting lines visually, you just see the offset and type 12j to jump 12 lines down. The current line still shows its absolute number.
System clipboard (unnamedplus) means yanking in Neovim puts text on the system clipboard and vice versa. No more "+y dance.
I also configure inline diagnostics for rust-analyzer and clippy with a clean visual style:
vim.diagnostic.config({
virtual_text = {
prefix = "●",
spacing = 2,
},
signs = true,
underline = true,
update_in_insert = false,
severity_sort = true,
})
Global Keymaps
The leader key is Space. I use jk to exit insert mode — faster than reaching for Escape.
vim.g.mapleader = " "
local keymap = vim.keymap
-- Exit insert mode
keymap.set("i", "jk", "<ESC>", { desc = "Exit insert mode with jk" })
-- Clear search highlights
keymap.set("n", "<leader>nh", ":nohl<CR>", { desc = "Clear highlights" })
-- Increment/decrement numbers
keymap.set("n", "<leader>+", "<C-a>", { desc = "Increment number" })
keymap.set("n", "<leader>-", "<C-x>", { desc = "Decrement number" })
Window & Tab Management
- <leader>svSplit window vertically
- <leader>shSplit window horizontally
- <leader>seEqualize split sizes
- <leader>sxClose current split
- <leader>toOpen new tab
- <leader>txClose tab
- <leader>tn / tpNext / Previous tab
- <leader>tfOpen current buffer in new tab
lazy.nvim
lazy.nvim is the plugin manager. It's fast, supports lazy-loading out of the box, and has a lockfile (lazy-lock.json) so installs are reproducible across machines. The bootstrap code auto-installs it if it's not already present:
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable",
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup("jaken.plugins")
That last line tells lazy.nvim to scan every file in lua/jaken/plugins/ and load them as plugin specs. Each file returns a table (or list of tables) describing one or more plugins.
Colorscheme — Catppuccin
I use Catppuccin with transparent background enabled so my terminal background shows through. This matters because I run this inside tmux, and I want the terminal's background to be consistent across panes.
return {
"catppuccin/nvim",
name = "catppuccin",
priority = 1000,
config = function()
require("catppuccin").setup({
transparent_background = true,
integrations = {
cmp = true,
gitsigns = true,
nvimtree = true,
treesitter = true,
telescope = { enabled = true },
},
})
vim.cmd.colorscheme("catppuccin")
end,
}
The priority = 1000 ensures the colorscheme loads before any other plugin tries to set highlight groups. The integrations table tells Catppuccin to provide theme support for specific plugins rather than relying on generic highlight groups.
Statusline — Lualine
lualine replaces Neovim's built-in statusline with something actually useful. I run a custom color theme that changes the mode indicator color based on what mode you're in — blue for Normal, green for Insert, violet for Visual, yellow for Command, red for Replace.
It also shows pending lazy.nvim updates in the statusline so I know when plugins need updating without having to check manually.
Buffer Tabs — Bufferline
bufferline adds VS Code-style tabs at the top. I have it running in tabs mode with a slant separator style. Simple, but it makes tab navigation visual rather than something you have to remember.
Dashboard — Alpha
When I open Neovim without a file, alpha-nvim shows a custom dashboard with a greeting (complete with cowboy emoji), the current date/time, and quick-launch buttons for common actions:
-- The dashboard header
dashboard.section.header.val = {
"🤠",
"",
"Howdy, Mr. Herman",
"Today is " .. os.date("%A") .. " " .. os.date("%d") ..
" " .. os.date("%B") .. " " .. os.date("%Y"),
"The time is " .. os.date("%H:%M"),
"",
"We've got cases to solve!"
}
The quick-launch buttons map directly to the keymaps I use most: new file, toggle file explorer, find file, find word, restore session, and quit.
Telescope
Telescope is the single most-used plugin in my setup. It's a fuzzy finder for everything: files, grep results, LSP references, diagnostics, todo comments. It replaces the need for a file browser in most cases.
I compile telescope-fzf-native for faster fuzzy matching and use C-j / C-k to navigate results (keeping my hands on the home row).
My Telescope Keymaps
- <leader>ffFind files in current directory
- <leader>frFind recent files
- <leader>fsLive grep — search text across project
- <leader>fcFind string under cursor
- <leader>fjFuzzy find in current buffer
- <leader>ftFind all TODO comments
- <leader><leader>Quick open file (like VS Code's Ctrl+P)
- gdGo to definition via Telescope
<leader><leader> mapping (double-tap Space) for quick file open is something I stole from the VS Code muscle memory of Shift+Shift in JetBrains IDEs. Extremely useful when you know the filename but not the path.
File Explorer — nvim-tree
nvim-tree is a sidebar file explorer. I keep it at 35 columns wide with relative line numbers (so I can jump with Vim motions even inside the tree). It shows indent markers, custom folder arrows, and hides .DS_Store files. Git-ignored files are shown (not hidden) because I often need to reference them.
- <leader>eeToggle file explorer
- <leader>efFind current file in explorer
- <leader>ecCollapse all folders
- <leader>erRefresh explorer
The window picker is disabled so that opening files from the tree always goes to the expected split, not whichever window happens to be focused. This makes nvim-tree work predictably alongside window splits.
Which-Key
which-key shows a popup of available keybindings when you start a key sequence and pause. If I press <leader> and wait 500ms, it shows every mapping that starts with Space. This is invaluable for learning your own config and for discovering mappings you've forgotten about.
Autopairs
nvim-autopairs automatically closes brackets, quotes, and parentheses. It integrates with both Treesitter (so it's context-aware about when to add pairs) and nvim-cmp (so accepting a completion that includes a function call also adds the closing paren).
Surround
nvim-surround lets you add, change, and delete surrounding characters. Select a word and surround it with quotes, change " to ', delete the wrapping () — all with a few keystrokes. Once you internalize the motions, it becomes one of those things you can't live without.
Substitute
substitute.nvim gives you a proper substitute operator. Yank some text, then use s{motion} to replace the target with what's in your register. ss substitutes the whole line, S substitutes to end of line. Faster than the :s/old/new/ command for quick replacements.
Comment
Comment.nvim with ts-context-commentstring means gcc comments out a line and gc in visual mode comments out a selection. The Treesitter integration is key — in a .tsx file, it knows to use {/* */} inside JSX and // outside it.
Mason & LSP Config
The LSP setup is split into two files. Mason handles installing language servers, and nvim-lspconfig configures them.
Installed Language Servers
Mason auto-installs these on first run:
-- Language servers
tsserver -- TypeScript / JavaScript
html -- HTML
cssls -- CSS
tailwindcss -- Tailwind CSS
svelte -- Svelte
lua_ls -- Lua
graphql -- GraphQL
emmet_ls -- Emmet (HTML/CSS shortcuts)
prismals -- Prisma ORM
pyright -- Python
-- Formatters & linters
prettier -- JS/TS/CSS/HTML/JSON/YAML/Markdown/GraphQL
stylua -- Lua
eslint_d -- JS/TS linting
LSP Keymaps
These only activate when an LSP server attaches to a buffer:
- gRShow references (Telescope)
- gDGo to declaration
- gdGo to definition (Telescope)
- giShow implementations (Telescope)
- gtShow type definitions (Telescope)
- <leader>caCode actions
- <leader>rnSmart rename
- <leader>DBuffer diagnostics (Telescope)
- <leader>dLine diagnostics (floating)
- KHover documentation
- [d / ]dPrevious / Next diagnostic
- <leader>rsRestart LSP
Autocompletion — nvim-cmp
nvim-cmp is the completion engine. It pulls from four sources in priority order:
- LSP — language server completions (types, functions, modules)
- LuaSnip — snippet expansions (via friendly-snippets)
- Buffer — words from the current buffer
- Path — file system paths
I use C-j / C-k to navigate the completion menu (home row), C-Space to manually trigger it, and Enter to confirm. The lspkind plugin adds VS Code-style icons to the completion menu so you can tell at a glance whether you're looking at a function, variable, snippet, or type.
Rustaceanvim
For Rust specifically, I use rustaceanvim instead of the generic lspconfig setup. It provides a tighter integration with rust-analyzer and comes preconfigured with Rust-specific features.
The key setting: I have clippy as the check-on-save command instead of the default cargo check. This means every time I save, clippy runs and surfaces lint warnings inline — not just compilation errors, but stylistic issues, performance suggestions, and common pitfalls.
vim.g.rustaceanvim = {
server = {
capabilities = require('cmp_nvim_lsp').default_capabilities(),
default_settings = {
['rust-analyzer'] = {
checkOnSave = true,
check = {
command = 'clippy',
extraArgs = { '--all', '--', '-W', 'clippy::all' },
},
},
},
},
}
The -W clippy::all flag enables all clippy lint groups, including pedantic and nursery lints that aren't on by default. This is aggressive, but I prefer catching things early.
Gitsigns
gitsigns adds git change indicators in the sign column (green for added, blue for changed, red for deleted) and current line blame — an inline annotation showing who last changed the line and when. The blame delay is set to 300ms so it doesn't flicker as you move around.
Git Keymaps
- ]h / [hNext / Previous hunk
- <leader>hsStage hunk
- <leader>hrReset hunk
- <leader>hS / hRStage / Reset entire buffer
- <leader>hpPreview hunk (floating diff)
- <leader>hbFull blame for line
- <leader>hdDiff current file
LazyGit
lazygit.nvim opens LazyGit in a floating terminal inside Neovim. <leader>lg and I have a full-featured Git TUI without leaving the editor. Staging, committing, rebasing, cherry-picking — all without typing git commands.
Formatting — Conform
conform.nvim handles formatting. Rather than relying on the LSP formatter (which can be slow or inconsistent), conform calls external formatters directly. My setup:
-- Formatter assignments
javascript/typescript/jsx/tsx/svelte → prettier
css/html/json/yaml/markdown/graphql → prettier
lua → stylua
python → isort + black
rust → rustfmt
Format-on-save is disabled. I prefer to format manually with <leader>mp when I'm ready. This avoids the jarring experience of your code jumping around while you're still thinking.
Treesitter
Treesitter provides real syntax highlighting (not regex-based) by building an AST of your code. The difference is especially noticeable in complex files like TSX or Svelte where multiple languages are embedded. I auto-install parsers for 17 languages.
Incremental selection is enabled: C-Space selects the current node, repeating expands the selection to the parent node, and Backspace shrinks it. This is faster than visual mode for selecting logical code blocks.
Trouble & Todo Comments
Trouble is a better quickfix list. It shows diagnostics, references, and TODOs in a structured panel. todo-comments highlights TODO, FIXME, HACK, etc. in your code and makes them searchable through both Telescope and Trouble.
- <leader>xwWorkspace diagnostics
- <leader>xdDocument diagnostics
- <leader>xtAll TODOs in Trouble
- ]t / [tNext / Previous TODO comment
Indent Blankline
indent-blankline draws subtle indent guides (┊) so you can visually track nesting depth. Essential in deeply nested code, and it's one of those things you don't appreciate until it's gone.
tmux + Neovim
I run Neovim inside tmux. The combination gives me persistent sessions, split panes for terminals alongside the editor, and the ability to detach and reattach from anywhere.
The critical glue is vim-tmux-navigator, which makes Ctrl-h/j/k/l navigate seamlessly between Neovim splits and tmux panes. Without this, you'd need separate key sequences for "move to the Neovim split on the left" vs. "move to the tmux pane on the left." With it, there's one set of keys and it just works — the plugin detects whether the current pane is running Neovim and routes the navigation accordingly.
~/.tmux.conf. See the plugin docs for the exact lines.
My typical tmux layout: one large pane running Neovim (usually 70-80% of the screen), a smaller pane below for running tests or cargo commands, and occasionally a third pane for a dev server or log tail. The Neovim splits handle code files, and the tmux panes handle everything else. Ctrl-h/j/k/l moves between all of them uniformly.
Session Management — Auto-Session
auto-session saves and restores your editing session per project directory. Close Neovim, come back the next day, and <leader>wr restores exactly where you left off — open files, splits, cursor positions, everything. Auto-restore is disabled (I prefer to trigger it explicitly), and common directories like ~/ and ~/Downloads are excluded so they don't pollute the session list.
Dressing
dressing.nvim replaces Neovim's built-in vim.ui.select and vim.ui.input with better-looking floating windows. Every time a plugin asks you to pick from a list or enter text, you get a Telescope-style picker instead of the default command-line prompt.
Vim Maximizer
vim-maximizer lets you temporarily maximize a split to full screen with <leader>sm, then restore it to its original size with the same key. Useful when you're working in a split but need to focus on one file for a moment.
How to Set This Up
If you want to run this exact config (or use it as a starting point):
Prerequisites
# Neovim 0.9+ (required for lazy.nvim)
brew install neovim
# tmux
brew install tmux
# Required for Telescope live grep
brew install ripgrep
# Required for telescope-fzf-native
brew install make
# Node.js (required for many LSP servers)
brew install node
# A Nerd Font (for icons in nvim-tree, lualine, etc.)
brew install --cask font-jetbrains-mono-nerd-font
Install
# Back up your existing config if you have one
mv ~/.config/nvim ~/.config/nvim.bak
# Clone the config
git clone https://github.com/jakenherman/nvim-config.git ~/.config/nvim
# Open Neovim — lazy.nvim will auto-install everything
nvim
On first launch, lazy.nvim bootstraps itself, then installs all plugins. Mason will then install the configured language servers and formatters. Give it a minute or two on the first run.
Verify
# Inside Neovim, check plugin status:
:Lazy
# Check installed LSP servers:
:Mason
# Check Treesitter parsers:
:TSInstallInfo
Complete Keymap Reference
General
- jkExit insert mode
- <leader>nhClear search highlights
- <leader>+ / -Increment / Decrement number
- <leader>mpFormat file or selection
- <leader>smMaximize / Restore split
Navigation
- Ctrl-h/j/k/lNavigate splits & tmux panes
- <leader>ffFind files
- <leader>fsLive grep
- <leader><leader>Quick open file
- <leader>eeToggle file explorer
- <leader>lgOpen LazyGit
LSP
- gd / gD / gRDefinition / Declaration / References
- gi / gtImplementations / Type definitions
- KHover docs
- <leader>caCode actions
- <leader>rnRename symbol
Git
- ]h / [hNext / Previous hunk
- <leader>hs / hrStage / Reset hunk
- <leader>hp / hbPreview hunk / Blame line
- <leader>hdDiff file
Session
- <leader>wrRestore session
- <leader>wsSave session
This config isn't done. It probably never will be. The whole point of a Neovim setup is that it evolves with how you work. But this is where it is today, and it's the most productive I've ever been in a text editor. If you have questions about any of it, feel free to reach out.