DEV Community

Cover image for Why I Built nvim-starter — a Neovim Config Beginners Can Actually Understand
Marius van Zundert
Marius van Zundert

Posted on

Why I Built nvim-starter — a Neovim Config Beginners Can Actually Understand

Kickstart.nvim gives you a working IDE. It also gives you lazy.nvim, nvim-cmp, telescope, nvim-treesitter, and a dozen concepts to learn before you've written a single line of config. I wanted something simpler.


The problem with kickstart.nvim

Let me be clear: kickstart.nvim is a good project. It's the standard recommendation for Neovim beginners, it's maintained by a core Neovim developer, and it ships a genuinely capable setup. I recommend it to people all the time.

But every time I did, I'd watch them hit the same wall. They'd clone the repo, launch Neovim, see plugins install, and then freeze. Not because the config didn't work — because they couldn't read it.

Here's what a beginner sees when they open kickstart.nvim's init.lua:

First, a large require('lazy').setup({...}) block with a DSL they've never seen before — event, cmd, keys, dependencies, opts, config. Before they've learned what vim.opt does, they're staring at lazy-loading triggers and event-driven plugin activation. Then nvim-cmp, which requires four separate source plugins (cmp-nvim-lsp, cmp-buffer, cmp-path, cmp_luasnip) and a configuration block that maps twenty keybindings, defines a formatting function, and wires up capabilities. Then telescope with its own setup and extension loading. Then nvim-treesitter with ensure_installed. Then mason-lspconfig with a manual ensure_installed list.

None of this is wrong — it's just a lot. It's a production config dressed as a starter. The very things that make kickstart powerful (lazy-loading, modular plugin specs, extensible completion sources) are the things that make it illegible to someone who doesn't yet know what a plugin is.

So I built nvim-starter. The goal: a configuration you can read top to bottom in ten minutes and walk away understanding.


What nvim-starter does differently

1. No plugin manager

This is the biggest decision I made, and the one that shapes everything else.

Kickstart.nvim uses lazy.nvim. lazy.nvim is excellent software — Folke is a brilliant developer, and lazy.nvim rightfully became the standard. But it's an abstraction layer between you and your plugins. To understand the config, you first have to understand lazy.nvim's spec format: what event = "VeryLazy" means, how cmd triggers work, why some plugins use dependencies and others don't, what the opts shorthand expands to.

For a beginner, this is backwards. They should be learning Neovim's concepts — buffers, windows, keymaps, options, LSP — not a plugin manager's DSL.

nvim-starter uses vim.pack.add(), which shipped with Neovim 0.12. You give it a list of GitHub URLs:

vim.pack.add({
  { src = "https://github.com/folke/tokyonight.nvim", name = "tokyonight.nvim" },
  { src = "https://github.com/folke/which-key.nvim",   name = "which-key.nvim" },
  { src = "https://github.com/folke/snacks.nvim",       name = "snacks.nvim" },
  -- ...
}, { confirm = false })
Enter fullscreen mode Exit fullscreen mode

That's it. There is nothing to learn. Neovim clones these repos into its standard plugin directory, adds them to the runtimepath, and loads them the same way it's loaded plugins for decades. The "plugin manager" is the editor itself. When a beginner reads this block, they see exactly what's happening: here are the plugins, here are their URLs, they get installed automatically.

No lazy-loading. No event triggers. No after/ directories. Ten plugins load at startup, and with vim.loader.enable() (bytecode caching introduced in Neovim 0.12), startup time is imperceptible. For a starter config with ten plugins, the complexity of lazy-loading isn't worth the cognitive cost.

2. blink.cmp instead of nvim-cmp

When kickstart.nvim was created, nvim-cmp was the only viable completion engine. It works, but it's a sprawling setup. You need:

  • nvim-cmp itself
  • cmp-nvim-lsp for LSP completions
  • cmp-buffer for buffer words
  • cmp-path for filesystem paths
  • cmp_luasnip for snippets
  • A capabilities bridge to wire it into the LSP client
  • A formatting function if you want pretty icons

That's five plugins and a configuration block that typically runs 40–60 lines.

blink.cmp — Saghen's Rust-based completion engine — bundles LSP, path, buffer, and snippet sources internally. The entire completion configuration in nvim-starter is 25 lines:

require("blink.cmp").setup({
  keymap = { preset = "none", ... },
  sources = { default = { "lsp", "path", "snippets", "buffer" } },
  snippets = { preset = "luasnip" },
})
Enter fullscreen mode Exit fullscreen mode

No separate source plugins. No capability bridging (one wildcard LSP config covers it). No formatting function. It's faster at runtime and dramatically simpler to read.

blink.cmp is younger than nvim-cmp and has a smaller ecosystem, but for the four sources a starter config needs — LSP, path, snippets, buffer — it's the simpler tool. I'll take simpler over battle-tested for something meant to be read and understood before it's used.

3. snacks.nvim over telescope (and toggleterm, and neo-tree)

telescope.nvim is a fantastic fuzzy finder. But it's only a fuzzy finder. If you also want a built-in terminal, you install toggleterm.nvim. A file explorer? neo-tree.nvim or nvim-tree.lua. Each of these has its own configuration syntax, its own keybindings, its own documentation to read.

Folke's snacks.nvim bundles all three — picker, terminal, explorer — into one plugin with one setup call. In nvim-starter, the entire UI layer is 10 lines:

require("snacks").setup({
  explorer = { enabled = true },
  terminal = { enabled = true },
  picker = { matcher = { fuzzy = true, smartcase = true, filename_bonus = true } },
})
Enter fullscreen mode Exit fullscreen mode

One plugin to understand instead of three. One set of keybindings to learn. One repo to trust. For a beginner who hasn't yet internalized the difference between a picker, a terminal buffer, and a file tree, this collapsing of concepts is a genuine learning aid.

4. No nvim-treesitter — just what Neovim ships

nvim-treesitter is another plugin that's excellent software and adds unnecessary complexity for beginners. It manages parser installations through :TSInstall, which means you're learning a plugin-specific command before you've learned :help. It has its own configuration syntax (ensure_installed, highlight = { enable = true }). It periodically prompts you about parser updates.

Neovim 0.12 ships with built-in Treesitter support and includes parsers for C, Lua, Markdown, Vimscript, vimdoc, and Query. That covers the languages most new Neovim users encounter first: Lua for their config, Markdown for notes, and maybe C if they're systems-curious.

For additional languages, you install parsers through your OS package manager:

$ apt install tree-sitter-python
$ apt install tree-sitter-rust
Enter fullscreen mode Exit fullscreen mode

Or via the tree-sitter CLI:

$ tree-sitter install python
Enter fullscreen mode Exit fullscreen mode

No Neovim plugin involved. No :TSInstall. The FileType autocmd in nvim-starter silently skips languages without parsers using pcall — no errors, no prompts, no confusion. When a beginner eventually installs a parser, highlighting just works.

This is a deliberate philosophical choice: parser management belongs to the OS, not to your editor's plugin system.

5. Zero per-server LSP configuration

In most Neovim configs — including kickstart — every language server needs its own setup block:

lspconfig.pyright.setup({ ... })
lspconfig.rust_analyzer.setup({ ... })
lspconfig.lua_ls.setup({ ... })
Enter fullscreen mode Exit fullscreen mode

Multiply this by however many languages you use, add capability merging and on_attach callbacks, and the LSP section alone can hit a hundred lines.

nvim-starter takes a different approach. mason-lspconfig's automatic_enable = true means: if a server binary exists on disk (because the user installed it through Mason's UI), start it automatically. No per-server blocks. No boilerplate.

Capabilities for blink.cmp are merged once, globally:

vim.lsp.config("*", {
  capabilities = require("blink.cmp").get_lsp_capabilities(...),
})
Enter fullscreen mode Exit fullscreen mode

One line. Every server, present and future, gets completion, signature help, and snippet capabilities. When I install pyright in Mason, it works. When I install rust_analyzer, it works. There is nothing to write, nothing to maintain, nothing to explain.

For beginners, this transforms the LSP experience from "read the lspconfig docs for your language" to "open Mason, press i on the server you want, restart neovim." That's the level of friction I wanted.

6. Single-file, by design

Kickstart.nvim starts as a single file but encourages modularization — its README suggests splitting into lua/plugins/, lua/opts/, lua/keymaps/. This is good practice for a growing config, but it's also the moment a beginner's understanding fragments. Suddenly the colorscheme is in one file, keymaps in another, and LSP config in a third, connected through require() calls that assume you understand Lua's module system.

nvim-starter's CONTRIBUTING.md states it plainly:

Stay single-file. The whole point is a single init.lua with comments.

The entire config is 363 lines, organized into 16 numbered sections. It starts with a comment block telling you the three most common tweaks (colorscheme, indent size, plugin list) and their line numbers. Section 0 enables the loader. Section 1 sets the leader key. Section 2 declares plugins. And so on, linearly, until section 16 enables Treesitter.

You read it top to bottom. You understand it. Then you fork it, rename it, and build your own. nvim-starter isn't a framework you extend — it's a reference implementation you learn from and outgrow.


Why I chose these specific plugins

Every plugin in nvim-starter passed a simple test: is this essential for a beginner, and is it the simplest implementation of what it does?

Plugin Why this one
tokyonight.nvim Dark theme, well-maintained, widely used. Gruvbox is commented-out as an alternative on the very next line.
which-key.nvim Shows available keybindings when you pause on <Space>. Beginners don't memorize keymaps — they discover them.
snacks.nvim One plugin replaces three (fuzzy finder, terminal, explorer). Less to install, less to configure, less to read.
mason.nvim + mason-lspconfig.nvim GUI server installer + auto-wiring. Beginners shouldn't need to know where binaries live or how cmd tables work.
nvim-lspconfig The standard LSP configuration library. Still needed for server-specific defaults, even with automatic_enable.
blink.cmp + blink.lib Rust-based completion that bundles its sources. Faster and simpler than nvim-cmp's source-plugin ecosystem.
gitsigns.nvim Git gutter markers. Beginners use Git. Seeing what changed is immediately useful.
guess-indent.nvim Auto-detects indentation per file. Beginners open files from projects they didn't create — this saves them from mixing tabs and spaces.
LuaSnip Snippet expansion. Powers the snippet source in blink.cmp. The only plugin with a build hook (compiles its regex engine on install).

That's 10 plugins, 11 if you count blink.lib (a dependency of blink.cmp). No file tree, no statusline plugin, no debug adapter, no auto-formatter beyond LSP formatting. Each omission is documented in docs/plugins.md with recommendations if you want to add them later.


What nvim-starter doesn't try to be

It's not your forever-config. It's deliberately undersized. Once you're comfortable with Neovim, you'll want things it doesn't include — a statusline, a debug adapter, auto-formatting on save, session management. That's by design. nvim-starter gets you to the point where you know what you want to add, which is the real goal.

It's not modular. There are no lua/ subdirectories, no require() chains, no plugin specs split across files. This is a constraint, not an oversight. Modularity is the right call for a configuration you'll maintain for years — but it's the wrong call for a configuration meant to be read in one sitting.

It's not version-agnostic. nvim-starter requires Neovim ≥ 0.12. This is a hard requirement and I'm comfortable with it. vim.pack.add() and native Treesitter are the right primitives for a beginner config, and they didn't exist before 0.12. Ubuntu 24.04 LTS ships Neovim 0.9.5, so you'll need a PPA, AppImage, or manual build. I think the trade-off — a simpler config that leverages editor-native features — is worth the install step. If Neovim 0.12 isn't available to you, use kickstart.nvim instead.


What you get in five minutes

Install it:

curl -fsSL https://raw.githubusercontent.com/Mvzundert/nvim-starter/main/install.sh | bash
Enter fullscreen mode Exit fullscreen mode

Open a file. Plugins clone for ~30 seconds. Then:

You get Press
Keybinding cheat sheet <Space> and wait
Fuzzy file finder <Space>sf
Content search (grep) <Space>sg
LSP server installer (GUI) <Space>cm
Terminal toggle <Space>tt
File explorer <Space>e
Go-to-definition gd (with LSP attached)
Hover docs K
Rename symbol <Space>rn
Code actions <Space>ca
Auto-completion Just type
Format buffer <Space>f

Install a language server by opening Mason (<Space>cm), pressing 2 for LSP servers, searching for your language, and pressing i. Reopen your file. Autocompletion, go-to-definition, and diagnostics all work. No config written. No docs read. Just a working IDE.


The trade-offs I'm comfortable with

Every design decision is a trade-off. Here are the ones I made and why I stand by them:

Neovim ≥ 0.12 only. Yes, this excludes stable-distribution users. But vim.pack.add() eliminates the plugin manager abstraction entirely. That single simplification justifies the version requirement. Beginners installing Neovim fresh aren't tied to a distro package anyway — they're following a "how to install Neovim" tutorial that gives them the latest version.

No lazy-loading. Ten plugins load at startup. On any machine built in the last decade, with vim.loader.enable() caching bytecode, this is under 50 milliseconds. The cognitive cost of explaining lazy-loading to a beginner vastly outweighs the performance gain.

blink.cmp instead of nvim-cmp. blink.cmp is newer and has a smaller plugin ecosystem. But a beginner doesn't need custom completion sources — they need LSP completions, buffer words, and file paths. blink.cmp does all three out of the box with a 25-line config. If they eventually need a source blink.cmp doesn't support, they'll have learned enough to evaluate nvim-cmp themselves.

No :LspInfo. Neovim 0.12 replaced :LspInfo with :checkhealth vim.lsp. This is documented in the README and in the inline comments, but I know it will trip up users following older tutorials. It's a Neovim upstream decision I can't control, and I think documenting it clearly is sufficient.

Built-in Treesitter requires extra steps for more languages. If you work in Python, Rust, Go, TypeScript, Bash, and ten other languages, installing parsers through your package manager is an extra step per language. nvim-treesitter's :TSInstall all is genuinely more convenient for polyglots. But nvim-starter is for beginners — beginners typically work in one or two languages. Installing one or two tree-sitter packages isn't a meaningful burden, and avoiding nvim-treesitter keeps the plugin count down and the mental model simple.


Why you might choose nvim-starter over kickstart.nvim

Use nvim-starter if:

  • You're new to Neovim and want to understand your config. Kickstart works, but it's hard to read. nvim-starter is 363 lines of commented Lua that tells you exactly what every line does and why it's there.
  • You don't want to learn a plugin manager before learning Neovim. lazy.nvim is powerful, but it's an abstraction. nvim-starter uses vim.pack.add() — there is no plugin manager to learn because the editor is the plugin manager.
  • You want to get productive in five minutes, not five hours. Install, launch, install one LSP server, start coding. That's the workflow. There's no config to write.
  • You learn by reading working code. nvim-starter is a reference implementation. Read it top to bottom. Then delete it and write your own — you'll know enough.
  • You prefer smaller dependency trees. Fewer plugins, fewer repositories to trust, fewer version interactions to debug.

Stick with kickstart.nvim if:

  • You need Neovim ≥ 0.9 compatibility (Ubuntu LTS, Debian stable, RHEL).
  • You want lazy-loading and fine-grained startup control.
  • You want nvim-cmp's mature plugin ecosystem.
  • You want :TSInstall convenience for dozens of languages.
  • You want a config you can grow modularly without rewriting from scratch.

The bottom line

I built nvim-starter because I believe the first Neovim configuration a beginner sees should be legible. They should be able to read it in one sitting, understand what every section does, and feel confident tweaking it. The plugins should auto-install without a plugin manager. The LSP should auto-wire without per-server configuration. The code should document itself.

Kickstart.nvim is a launchpad. nvim-starter is a tutorial that happens to give you a working IDE.


Repository: github.com/Mvzundert/nvim-starter

Requirements: Neovim ≥ 0.12, Git, Nerd Font (optional)

License: MIT

Top comments (0)