The Vim Hate is Actually About Bad Config
You’ve heard it a hundred times: “Vim has a steep learning curve.” What they actually mean is “Vim with a garbage config has a steep learning curve.” Throw in six months of plugin hunting, a bloated init.vim with conflicting keybindings, and LSP wired up incorrectly, and yeah—you’ll hate it.
Here’s the thing: Neovim in 2026 is not your dad’s Vim. Native LSP support, Lua as first-class config language, and plugin managers that actually work mean you can have a solid, productive editor in a week. Not six months. Not six years.
This is the setup I’ve been using for two years. It’s minimal enough that you can understand every line, powerful enough that you won’t outgrow it, and boring enough that you’ll stop thinking about config and start thinking about your actual code.
The Philosophy: Start Minimal, Add When It Hurts
Don’t start with a framework (looking at you, LunarVim and Doom Emacs clones—wrong editor anyway). Don’t grab someone’s 2000-line config and cargo-cult it into your ~/.config/nvim/.
Start with 100 lines. Use the editor for a week. When something hurts, add a plugin. Repeat.
This config follows that exact pattern. You get:
- Sane defaults (no arrow keys, proper tabs, incremental search)
- LSP hooked up for any language you touch
- lazy.nvim for sane plugin management (loads on-demand, not at startup)
- A keymap structure that makes sense
- About 150 lines total
After a week, you’ll know exactly what you actually need.
The Directory Structure
~/.config/nvim/ init.lua ← entry point, ~50 lines lua/config/ settings.lua ← options, behavior keymaps.lua ← all keybindings lazy.lua ← lazy.nvim bootstrap lua/plugins/ lsp.lua ← LSP client setup telescope.lua ← fuzzy finder (optional) other-plugins.lua ← everything elseSimple. Each file has one job. No nested subdirs you’ll forget about.
Part 1: The Entry Point (init.lua)
This file bootstraps everything:
-- Load configuration modulesrequire("config.settings")require("config.keymaps")require("config.lazy")
-- Load plugins (lazy.nvim handles the actual loading)require("lazy").setup(require("plugins"))That’s it. The actual work lives in the modules.
Part 2: Settings (lua/config/settings.lua)
These are the opinionated defaults that make Neovim actually usable:
local opt = vim.opt
-- Tabs and indentationopt.tabstop = 2opt.shiftwidth = 2opt.expandtab = trueopt.smartindent = true
-- Displayopt.number = trueopt.relativenumber = false -- use absolute, not relative (faster for jumping)opt.wrap = falseopt.cursorline = trueopt.signcolumn = "yes"opt.termguicolors = true
-- Search behavioropt.ignorecase = trueopt.smartcase = trueopt.incsearch = trueopt.hlsearch = false -- don't leave highlight on after search
-- Performance and behavioropt.hidden = true -- allow switching buffers without savingopt.undofile = true -- persistent undoopt.mouse = "a" -- why not?opt.clipboard = "unnamedplus" -- use system clipboard
-- Minimal UI cruftopt.showcmd = falseopt.showmode = falseopt.ruler = falseopt.laststatus = 2
-- Folding (leave it off unless you want it)opt.foldenable = falseNothing fancy. This gives you incremental search, persistent undo, clipboard access, and a clean UI. It’s what you’d expect from an editor written in the last decade.
Part 3: Keymaps (lua/config/keymaps.lua)
Vim’s defaults are optimized for a 1987 keyboard layout. Here’s the minimal sane set:
local map = vim.keymap.set
-- Leader key (use space, it's huge and easy to hit)vim.g.mapleader = " "vim.g.maplocalleader = " "
-- Disable arrow keys (yes, really—forces muscle memory)map("", "<Up>", "<Nop>")map("", "<Down>", "<Nop>")map("", "<Left>", "<Nop>")map("", "<Right>", "<Nop>")
-- Splits and windowsmap("n", "<Leader>v", "<C-w>v", { noremap = true }) -- vertical splitmap("n", "<Leader>h", "<C-w>s", { noremap = true }) -- horizontal splitmap("n", "<Leader>w", "<C-w>w", { noremap = true }) -- switch windowmap("n", "<Leader>q", "<C-w>q", { noremap = true }) -- close window
-- Buffersmap("n", "<Leader>bn", ":bnext<CR>", { noremap = true }) -- next buffermap("n", "<Leader>bp", ":bprev<CR>", { noremap = true }) -- prev buffermap("n", "<Leader>bd", ":bdelete<CR>", { noremap = true }) -- delete buffer
-- LSP (gets extended in lsp.lua)map("n", "gd", vim.lsp.buf.definition, { noremap = true }) -- go to definitionmap("n", "gr", vim.lsp.buf.references, { noremap = true }) -- find referencesmap("n", "K", vim.lsp.buf.hover, { noremap = true }) -- hover docsmap("n", "<Leader>rn", vim.lsp.buf.rename, { noremap = true })-- rename symbol
-- Quick thingsmap("n", "<Esc>", ":nohl<CR>", { noremap = true }) -- clear search highlightmap("n", "j", "gj", { noremap = true }) -- move by visual linemap("n", "k", "gk", { noremap = true })That’s 25 keybindings. You don’t need 200. These cover: window management, buffer switching, and LSP basics. Everything else you’ll add as you discover you need it.
Part 4: Lazy Plugin Manager Bootstrap (lua/config/lazy.lua)
This bootstraps lazy.nvim, which is the only plugin manager that actually makes sense in 2026:
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or 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(require("plugins"), { install = { colorscheme = { "tokyonight" } },})That’s it. It clones lazy.nvim on first run, sets up the runtime path, and kicks off plugin loading. Plugins live in lua/plugins/.
Part 5: Plugins (lua/plugins/)
Create a lua/plugins/ directory with individual plugin files. Start with the absolute essentials:
lua/plugins/lsp.lua — The Secret Sauce
LSP is what makes modern editors useful. Here’s the minimal setup:
return { { "neovim/nvim-lspconfig", event = { "BufReadPre", "BufNewFile" }, dependencies = { "hrsh7th/cmp-nvim-lsp", }, config = function() local lspconfig = require("lspconfig") local capabilities = require("cmp_nvim_lsp").default_capabilities()
-- Lua (via lua_ls) lspconfig.lua_ls.setup({ capabilities = capabilities, settings = { Lua = { runtime = { version = "LuaJIT" }, diagnostics = { globals = { "vim" } }, }, }, })
-- Python (via pylsp or pyright) lspconfig.pylsp.setup({ capabilities = capabilities })
-- JavaScript/TypeScript (via ts_ls) lspconfig.ts_ls.setup({ capabilities = capabilities })
-- Rust (via rust_analyzer) lspconfig.rust_analyzer.setup({ capabilities = capabilities })
-- Go (via gopls) lspconfig.gopls.setup({ capabilities = capabilities })
-- JSON lspconfig.jsonls.setup({ capabilities = capabilities })
-- YAML lspconfig.yamlls.setup({ capabilities = capabilities }) end, }, -- Autocompletion { "hrsh7th/nvim-cmp", event = "InsertEnter", dependencies = { "hrsh7th/cmp-nvim-lsp", "hrsh7th/cmp-buffer", "hrsh7th/cmp-path", "hrsh7th/cmp-cmdline", }, config = function() local cmp = require("cmp") cmp.setup({ mapping = cmp.mapping.preset.insert({ ["<C-b>"] = cmp.mapping.scroll_docs(-4), ["<C-f>"] = cmp.mapping.scroll_docs(4), ["<C-Space>"] = cmp.mapping.complete(), ["<C-e>"] = cmp.mapping.abort(), ["<CR>"] = cmp.mapping.confirm({ select = true }), ["<Tab>"] = cmp.mapping(function(fallback) if cmp.visible() then cmp.select_next_item() else fallback() end end, { "i", "s" }), }), sources = cmp.config.sources({ { name = "nvim_lsp" }, { name = "buffer" }, { name = "path" }, }), }) end, },}This does three things:
- Sets up LSP clients for common languages (you’ll install the actual servers separately via your package manager or
:LspInstall) - Wires in autocompletion powered by LSP
- Maps Tab to cycle through completions
Install language servers with your package manager:
# macOS (Homebrew)brew install lua-language-server pylsp typescript-language-server gopls rust-analyzer
# Archsudo pacman -S lua-language-server python-pylsp typescript-language-server go gopls rust
# Ubuntu/Debiansudo apt install lua-language-server pylsp nodejs npmnpm install -g typescript typescript-language-server# etc.lua/plugins/telescope.lua — Fuzzy Finder
One plugin that earns its weight:
return { { "nvim-telescope/telescope.nvim", tag = "0.1.8", dependencies = { "nvim-lua/plenary.nvim" }, config = function() local telescope = require("telescope.builtin") vim.keymap.set("n", "<Leader>ff", telescope.find_files, {}) vim.keymap.set("n", "<Leader>fg", telescope.live_grep, {}) vim.keymap.set("n", "<Leader>fb", telescope.buffers, {}) vim.keymap.set("n", "<Leader>fh", telescope.help_tags, {}) end, },}This gives you fuzzy file search (<Leader>ff), ripgrep text search (<Leader>fg), buffer search, and help tag search. That’s 90% of what you actually use.
lua/plugins/colorscheme.lua — One Less Thing to Think About
return { { "folke/tokyonight.nvim", lazy = false, priority = 1000, config = function() vim.cmd([[colorscheme tokyonight-night]]) end, },}One good colorscheme. No debate. Done.
lua/plugins/init.lua — Plugin Index
return { require("plugins.lsp"), require("plugins.telescope"), require("plugins.colorscheme"), -- Add more as you discover you need them}Part 6: Actually Using This Thing
-
Create the structure:
Terminal window mkdir -p ~/.config/nvim/lua/config ~/.config/nvim/lua/plugins -
Copy the files above into their respective paths.
-
Install language servers for the languages you actually use (see the LSP section above).
-
Open nvim:
Terminal window nvimlazy.nvim will auto-install plugins on first run. It’ll take 30 seconds.
-
Try the basics:
<Leader>ffto find a file<Leader>fgto grep for textgdto jump to a function definition (if LSP is running)Kto see hover docs:LspInfoto check if your language server is actually running
If LSP isn’t showing up, make sure you installed the server for your language and Neovim can find it in your $PATH.
The Decision: Should You Use This?
You should if: You want a real editor that doesn’t require a PhD in configuration. You’re tired of VSCode’s bloat (1GB RAM on day one). You like tinkering but hate evangelizing. You spend more time in terminals than GUIs. You work on a 10-year-old laptop where every MB counts.
You probably shouldn’t if: You need a debugger GUI (nvim-dap exists but it’s not that). You’re not comfortable with the command line. Your team all uses VSCode and you need to fit in. You care more about clicking buttons than understanding your tools.
What You Don’t Get (And Why That’s Fine)
- Git UI: There’s Neogit, but honestly?
gitin a terminal pane works fine. Fewer abstractions to debug. - Full debugger: nvim-dap exists, but you’re probably better off with
gdbin another terminal. - Integrated terminal multiplexing: Use tmux. It’s better at it.
- A GUI: You have one. It’s a rectangle that shows text. That’s the feature.
One Year Later
In a year, you’ll have added maybe 10 more plugins. You’ll understand Lua well enough to write custom keybindings. You’ll have tuned LSP settings for your specific languages. You won’t think about the editor anymore—you’ll just use it.
That’s the point. The goal is to spend less time configuring and more time shipping.
Start here. Add one thing at a time when it hurts. Don’t be the person with 500 lines of config you don’t understand. Be the person with 200 lines you wrote yourself and can explain to someone else.
Your future self (the one debugging that 2 AM bug) will thank you.