The last time I used Vim extensively was in the mid-2010s, a little before Neovim really surged in popularity. Having done the rounds of JetBrains IDEs, VS Code, Zed, and, most recently, Helix, I held an unconscious estimation that Vim had been relegated to a former time. Yes, Neovim had come and modernised it, and yes, it kept being name-dropped, but these new editors did everything I wanted.
Until they didn’t. When my day job shifted off the JVM, IntelliJ IDEA
stopped being an obvious choice. VS Code served me for a while but it
felt sluggish. Zed served me for even longer, but it ended striking this
weird balance between being opinionated while also needing quite a bit
of configuration, while also lacking certain features just because it
was newer editor; I remember the joy when Git support finally dropped!
Helix, meanwhile, worked well almost out of the box: just configure
which language server you want and get going. But the key bindings never
really clicked for me (C, delete from cursor
position to the end of the line, was one of my most-used Vim key
bindings and sorely missed in Helix).
I stuck with Helix, but the issues mounted. The breaking point was me wanting to resolve the name of a Python function and the current file name and pass them to a shell script, and being unable to do this easily. A look at Helix’s plugin support story—which hasn’t felt like it’s moved in a long time—led me to right to Neovim. Plugin support? Check. Key bindings I’m accustomed to? Check. Easy to configure and understand? Not exactly, from what I can remember, but let’s see how the landscape is in 2026.
My memory of Vim is that it was necessary to have at least a few plugins installed. I also remember them being difficult to manage and configure. And so I was thrilled when I found that modern Neovim is really easy to configure. It’s a bit more work than Helix, but not much. Let’s take a look.
lazy.nvim is a
plugin manager for Neovim that handles download, installation, updates,
load order, configuration… it does the lot. With over 20,000 stars on
GitHub, it’s well regarded, and you can see why with how easy it is to
set up. Lua-based Neovim configuration begins in the
nvim/init.lua file in your config directory. Add this:
vim.g.mapleader = ' '
vim.g.maplocalleader = ' '
require("config.lazy")And then to nvim/lua/config/lazy.lua, add:
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
-- Execute only if lazy.nvim directory isn't found
if not vim.uv.fs_stat(lazypath) then
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({
"git", "clone", "--filter=blob:none", "--branch=stable",
lazyrepo, lazypath
})
-- If clone fails, wait for user to read message, then exit neovim
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
-- Prepend lazy.nvim to runtime path so it runs before other plugins
vim.opt.rtp:prepend(lazypath)
require("lazy").setup("plugins")How many plugins are needed to have a Helix-like experience? 8, by my
count. This is my nvim directory:
.
├── init.lua
├── lua
│ ├── config
│ │ └── lazy.lua
│ └── plugins
│ ├── blink.lua
│ ├── catppuccin.lua
│ ├── gitsigns.lua
│ ├── mini-icons.lua
│ ├── nvim-lspconfig.lua
│ ├── nvim-treesitter.lua
│ ├── snacks.lua
│ └── which-key.lua
Let’s take a tour of these in the order I installed them.
The very first thing I wanted was solid syntax highlighting. Enter Tree-sitter, a
tool to generate parsers for most programming and markup languages under
the sun. While the functionality isn’t built into Neovim, the plugin is
officially supported. This is my
nvim/lua/plugins/nvim-treesitter.lua file:
local file_types = {
'c',
'html', 'css', 'javascript',
'lua', 'luadoc', 'python',
'bash', 'diff', 'vimdoc', 'markdown', 'markdown_inline',
}
return {
{
'nvim-treesitter/nvim-treesitter',
ft = file_types,
lazy = false,
build = ':TSUpdate',
config = function()
require('nvim-treesitter').install(file_types)
vim.api.nvim_create_autocmd('FileType', {
pattern = file_types,
callback = function() vim.treesitter.start() end,
})
end,
},
}This configuration will automatically install the parsers for the languages listed and start Tree-sitter whenever that a buffer of one of those file types is entered.
Next, I wanted language
server protocol support so I get comprehensive code diagnostics. The
second official plugin on the list
takes care of that. Here’s
nvim/lua/plugins/nvim-lspconfig.lua:
return {
{
'neovim/nvim-lspconfig',
config = function()
vim.lsp.config('ty', { cmd = { 'ty', 'server' } })
vim.lsp.config('ruff', { cmd = { 'ruff', 'server' } })
vim.lsp.enable('ty')
vim.lsp.enable('ruff')
end,
}
}As long as the language servers can be found in the path (run
which ty to confirm, for example), this will add
go-to-definition support (gti in Normal Mode), a
documentation pop-up (K in Normal Mode), and the like. For
Python projects, I tend to add ty and ruff as
dev dependencies and source the Python virtual environment into my shell
before starting Neovim, which inserts their binaries into the path.
The final big component was a file picker. snacks.nvim does that, and a whole lot more. I’ve disabled most of its other functionality; I’ll selectively enable each part as I learn more about them.
return {
{
'folke/snacks.nvim',
priority = 1000,
opts = {
animate = { enabled = false },
bigfile = { enabled = false },
-- long list of other disabling lines...
-- see repo link at the end of this post for the full list
notify = { enabled = false },
picker = { enabled = true, icons = { mappings = true } },
profiler = { enabled = false },
-- and more of them...
zen = { enabled = false },
},
keys = {
{ "<leader><Space>", function() Snacks.picker.smart() end, desc = "Smart Find Files" },
{ "<leader>,", function() Snacks.picker.buffers() end, desc = "Buffers" },
{ "<leader>/", function() Snacks.picker.grep() end, desc = "Grep" },
-- Find
{ '<leader>ff', function() Snacks.picker.files() end, desc = "[f]ind [f]iles" },
-- Search
{ '<leader>sd', function() Snacks.picker.diagnostics_buffer() end, desc = "[s]earch Buffer [d]iagnostics" },
{ '<leader>sh', function() Snacks.picker.help() end, desc = "[s]earch [h]elp Pages" },
{ '<leader>sk', function() Snacks.picker.keymaps() end, desc = "[s]earch [k]eymaps" },
{ '<leader>s"', function() Snacks.picker.registers() end, desc = "[s]earch Registers" },
},
}
}Snacks has a lot of really useful pickers which I’ll add key bindings for as and when the need arises for them. This setup will get me started and replicates the ones I most commonly used in Helix.
Snacks supports Nerd Font glyphs and icons. I installed the Departure Mono font (which I now adore) and the mini.icons icon provider:
return {
{
'nvim-mini/mini.icons',
version = false,
config = function() require('mini.icons').setup() end,
}
}Naturally, a colour scheme was needed. Catppuccin was right on-vibe:
return {
{
'catppuccin/nvim',
name = "catppuccin",
priority = 1000,
config = function() vim.cmd.colorscheme('catppuccin') end,
}
}The setup is rounded off by three quality of life improvements. First, which-key.nvim shows a pop-up when in the middle of a key chord.
return {
{
'folke/which-key.nvim',
event = "VeryLazy",
opts = {
icons = { mappings = true },
},
keys = {
{ "<leader>?",
function() require('which-key').show({ global = false }) end,
desc = 'Buffer Local Keymaps (which-key)',
},
},
}
}Second, gitsigns.nvim displays a Git gutter and inline Git blame at the cursor line.
return {
{
'lewis6991/gitsigns.nvim',
opts = {
current_line_blame = true,
},
}
}Third, blink.cmp add auto-completions.
return {
{
'saghen/blink.cmp',
version = '1.*',
opts = {
sources = { default = { 'lsp' }},
fuzzy = { implementation = 'rust' },
signature = { enabled = true },
}
}
}I change a default values on a few Vim options like setting the line number, enabling smart-case search, and disabling non-Lua language providers (I plan to only use Lua plugins). Most of these were based on kickstart.nvim’s overrides.
Check out my repository for the full config.
~K