My Neovim Setup: A Kickstart Config I Actually Understand
A walkthrough of my real Neovim config: 31 plugins on a Kickstart base, set up for C#/.NET, web dev, and writing. What I run and why.
I rebuilt my Neovim config from scratch this year. Not because the old one broke. Because I couldn't tell you what half of it did.
That's the trap with editor configs. You copy a YouTuber's dotfiles, it looks incredible, and then six months later something throws an error on startup and you're staring at Lua you've never read. So this time I started from Kickstart.nvim and built up from there, one file at a time, only adding things I actually use.
Here's the whole setup: what I run, why I run it, and the parts that earn their place every single day.
Why Kickstart and not LazyVim
LazyVim is great. It's a polished, batteries-included distribution that turns Neovim into something close to a full IDE the moment you install it. A lot of people love it and they're not wrong to.
I didn't want that. I wanted to read every line.
Kickstart isn't a distribution. It's a single well-commented starting config that you're meant to edit, break, and grow yourself. You don't inherit a framework. You inherit a head start. When I open my config now, there's nothing in it I can't explain, because I put it there.
That decision pays off the first time something goes sideways. A LazyVim error means digging through layers of someone else's abstraction. A Kickstart error means opening the one file I wrote and fixing it. If you're coming from VS Code and weighing your options, I went deep on the editor-switching question in my honest walkthrough of moving from VS Code to Zed, and the same logic applies here: pick the tool you'll actually understand.
The structure: one file per plugin
This is the single best decision in the whole config and it's almost too simple.
My init.lua holds my core options and keymaps, then ends with one line that loads a whole folder:
require('lazy').setup({
{ import = 'custom.plugins' },
})
Every plugin lives in its own file under lua/custom/plugins/. telescope.lua, lspconfig.lua, conform.lua, gitsigns.lua, and so on. lazy.nvim picks them all up automatically.
Why bother? Three reasons.
→ When something breaks, I know which file to open. The git plugin misbehaves, I open gitsigns.lua. Done.
→ Removing a plugin means deleting one file. No hunting through a monolith for the right block to comment out.
→ Git diffs stay clean. Each change touches one small file, so my commit history actually reads like a story.
Right now that folder holds about 18 plugin files and the lock file pins 31 packages total. Most of those 31 are dependencies I never named. The stuff I chose deliberately is a much shorter list.
The core that does the real work
If I had to rebuild on a fresh machine with only six plugins, these are the six.
Telescope is how I move. Fuzzy file finding, live grep across the project, searching help docs, jumping to LSP references. <leader>sf finds files, <leader>sg greps the whole project, and <leader><leader> flips between open buffers. I added a tiny shortcut, <leader>sn, that searches my Neovim config files specifically, because I'm in there often enough to want a fast door. It's backed by fzf-native compiled with make, so it's quick even on big repos.
LSP via nvim-lspconfig and Mason is the brains. Mason installs and manages the language servers so I'm not fighting my system package manager. My install list is shaped around what I actually build: lua_ls for the config itself, omnisharp and a C# server for .NET, html, css, and json servers for web work, and bicep for Azure infrastructure. The keymaps follow the newer Neovim convention: grn to rename, gra for code actions, grr for references, grd for definition.
blink.cmp handles completion. I switched to it from nvim-cmp and haven't looked back. It's fast, the defaults are sane, and it wires into LuaSnip for snippets and into lazydev for Lua development. Signature help is on, documentation popups are off by default so they don't crowd the screen.
Treesitter does syntax highlighting and indentation properly, by actually parsing the code instead of guessing with regex. I'm on the new main branch, which changed how this works: you start Treesitter per buffer yourself and it auto-installs a missing parser the first time you open a new filetype. I keep parsers pinned for the languages I touch most, including c_sharp, bicep, markdown, lua, css, and html.
conform.nvim formats on save. This is the one that quietly keeps my code clean without me thinking about it. Lua gets stylua, JavaScript gets prettierd or prettier, Python gets isort then black, and C# gets csharpier. I can also hit <leader>f to format on demand. Format-on-save is the kind of thing you don't notice until you turn it off and realize how much manual cleanup you'd been doing.
gitsigns puts git in the gutter and gives me a full hunk workflow without leaving the buffer. ]c and [c jump between changes, <leader>hs stages a hunk, <leader>hp previews one, <leader>hb blames the current line. For anything bigger I still drop to the terminal, but for the small stuff this keeps me in flow.
The comfort layer
Past the core, these are the plugins that make the editor feel like mine.
tokyonight, fully transparent. I run my terminal with a background image and I want Neovim to sit on top of it, so I force transparency on a list of highlight groups including Normal, NormalFloat, SignColumn, and the rest. Italic comments off, because I find them harder to read.
which-key with the popup delay set to zero. The second I hit space, it shows me every keybinding hanging off it. This is how I actually remember my own keymaps. No delay means no waiting, it just appears.
mini.nvim, three modules: mini.ai for smarter text objects (so va) selects around parens, ci' changes inside quotes), mini.surround, and mini.statusline for a clean status line that doesn't need a nerd font. I also run nvim-surround separately because its ys/ds/cs motions are burned into my muscle memory. Yes, that's two surround plugins. I know. It's on my cleanup list.
nvim-lint runs markdownlint on markdown files. This one matters more than it sounds, because I write most of my blog posts inside Neovim. Linting as I write keeps the markdown tidy before it ever hits the CMS.
indent-blankline, todo-comments, and nvim-autopairs round it out. Quiet plugins that do one job. The indent guides help me read nested Lua and JSX, todo-comments highlights my TODO and HACK notes so they don't get lost, and autopairs closes my brackets.
nvim-dap for debugging, currently wired for Go with delve. I don't use it daily, but when I need a breakpoint and a step-through, <F5> starts it and a real debugger UI opens up.
The options that matter more than the plugins
People obsess over plugins and ignore the settings. The settings are where comfort actually lives. A few I'd never give up:
relativenumberon. Jumping8kor12jis only fast when you can see the 8 and the 12.scrolloffset to 15, so my cursor line stays near the middle of the screen instead of riding the bottom edge.undofileon. Undo history survives closing the file. This has saved me more than once.clipboardset tounnamedplus, so yanking in Neovim copies to the system clipboard. No more separate copy step.- Space as the leader key. It's the biggest, easiest key to hit with either thumb.
- Two-space indents with
expandtab, because that's the house style across the web and Lua code I write.
If any of this feels foreign, the motions and movement side is its own skill worth building first. I broke that down in my guide to Vim, Neovim, and motion commands, and honestly the motions matter more than any plugin on this list.
What it's actually set up for
This config isn't generic. It's shaped around the work I do.
The C# and bicep language servers are there because I build .NET applications and deploy them to Azure. If you're in that world, Neovim handles it better than most people expect, and I wrote up a recent project in building a .NET Core MVC platform if you want to see what that looks like in practice.
The html, css, json servers plus prettier are there for web work. And the markdownlint setup is there because I draft articles right here in the editor, the same way I drafted this one.
What to do now
If you've been putting off setting up Neovim because the configs look terrifying, do this: clone Kickstart, delete the single-file version, and split it into one file per plugin like I did. Add nothing on day one. Live in it for a week. Only then start pulling in plugins, and only the ones that solve a problem you actually hit.
The goal isn't the prettiest config on the internet. It's a config you can open at 11pm with a broken startup and fix in two minutes, because you wrote every line of it.
My whole setup lives in a git repo, so a fresh machine is a clone and a two-minute wait. If you're also rethinking your editor and weighing the alternatives, my take on why I almost dropped VS Code for Zed covers the other side of that decision.
Building scalable systems and developer-first tools. Lead Software Engineer at DSRPT.
Frequently asked
-
Use Kickstart if you want to understand your config and grow it yourself. It ships a single, readable starting point you edit directly, so nothing is hidden. Use LazyVim if you want a full IDE experience out of the box and you're fine treating most of it as a black box. I went with Kickstart because I'd rather own every line than debug someone else's abstraction at 11pm.
-
Yes. Install the omnisharp or csharp language server through Mason, add csharpier as your formatter, and you get completion, go-to-definition, rename, and code actions for C#. I also run the bicep language server for Azure infrastructure files. It's not full Visual Studio, but for day-to-day editing and refactoring it holds up fine.
-
Fewer than you think. My config runs 31 plugins and most of those are dependencies, not things I chose one by one. The core that earns its keep is a fuzzy finder (Telescope), LSP (nvim-lspconfig plus Mason), completion (blink.cmp), Treesitter, a formatter (conform.nvim), and git signs. Everything else is comfort, not necessity.
-
One file per plugin keeps things findable and keeps git diffs clean. My init.lua has one line that imports a whole folder, and every plugin lives in its own file under lua/custom/plugins/. When something breaks I know exactly which file to open, and I can delete a plugin by deleting one file instead of hunting through a 2000-line monolith.