Neovim config for 2025

I wrote about setting up a “modern” Neovim config about 2.5 years ago. In that post, I put a ton of effort into figuring out all the then-new Neovim features, Treesitter, LSP, and converted my config to Lua.

And now, everything has changed again and that post is completely irrelevant. Great. But thankfully since Neovim 0.11 it’s all much easier, half the plugins aren’t needed any more, and the remaining ones are quite simple to setup. But all the configs you see shared online are huge and complicated. So I’m sharing my simple one.

TLDR: Here’s the 180 line Neovim config as a Github Gist.

# Getting started

  1. You need to install Neovim v0.11+. On macOs this is brew install neovim. On Ubuntu, the default apt install neovim is too old, but the snap install neovim is up-to-date.
  2. Then place your config in ~/.config/nvim/init.lua.
  3. Then run nvim! There will be lots of messages. Give it a beat, quit, run it again.

# Basic settings

This hasn’t changed much. Feel free to ask your neighbourhood LLM what all of these do if you have any questions. Most of these are pretty unambiguously useful. Some are worth familiarising yourself with. The persistent undo history is probably the only clever thing here: killer feature to be able to quit and re-open Neovim and continue undoing/redoing.

-- Basic settings
vim.opt.hlsearch = true
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.mouse = "a"
vim.opt.showmode = false
vim.opt.spelllang = "en_gb"

-- Leader (this is here so plugins etc pick it up)
vim.g.mapleader = ","  -- anywhere you see <leader> means hit ,

-- use nvim-tree instead
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1

-- Use system clipboard
vim.opt.clipboard:append({ "unnamed", "unnamedplus" })

-- Display settings
vim.opt.termguicolors = true
vim.o.background = "light" -- set to "dark" for dark theme

-- Scrolling and UI settings
vim.opt.cursorline = true
vim.opt.cursorcolumn = true
vim.opt.signcolumn = 'yes'
vim.opt.wrap = false
vim.opt.sidescrolloff = 8
vim.opt.scrolloff = 8

And it keeps going…

-- Title
vim.opt.title = true
vim.opt.titlestring = "nvim"

-- Persist undo (persists your undo history between sessions)
vim.opt.undodir = vim.fn.stdpath("cache") .. "/undo"
vim.opt.undofile = true

-- Tab stuff
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
vim.opt.autoindent = true

-- Search configuration
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.gdefault = true

-- open new split panes to right and below (as you probably expect)
vim.opt.splitright = true
vim.opt.splitbelow = true

-- LSP
vim.lsp.inlay_hint.enable(true)

# Plugins

This is a relatively constrained list of useful plugins. In short:

This is just enough to get “IDE” functionality and some niceties that you’re probably used to from VSCode/similar.

local plugins = {
  { "nvim-lua/plenary.nvim" },       -- used by other plugins
  { "nvim-tree/nvim-web-devicons" }, -- used by other plugins

  -- Gruvbox theme (feel free to choose another!)
  { "ellisonleao/gruvbox.nvim" },
  
  { "nvim-lualine/lualine.nvim" },  -- status line
  { "nvim-tree/nvim-tree.lua" },    -- file browser

  -- Telescope command menu
  { "nvim-telescope/telescope.nvim" },
  { "nvim-telescope/telescope-fzf-native.nvim", build = "make" },

  -- TreeSitter
  { "nvim-treesitter/nvim-treesitter", build = ":TSUpdate" },

  -- LSP stuff
  { 'mason-org/mason.nvim' },          -- installs LSP servers
  { 'neovim/nvim-lspconfig' },         -- configures LSPs
  { 'mason-org/mason-lspconfig.nvim' },-- links the two above

  -- Some LSPs don't support formatting, this fills the gaps
  { 'stevearc/conform.nvim' },

  -- Autocomplete engine (LSP, snippets etc)
  -- see keymap:
  -- https://cmp.saghen.dev/configuration/keymap.html#default
  {
    'saghen/blink.cmp',
    version = '1.*',
    opts_extend = { "sources.default" }
  },
}

Then you need to bootstrap lazy (the plugin manager) and install the plugins:

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(plugins)

# Some plugin configuration

Now that the plugins are installed, you need to configure them. Google these plugins if you want to see how to customise them, but the defaults are good enough to get started with!

vim.cmd.colorscheme("gruvbox")  -- activate the theme
require("lualine").setup()      -- the status line
require("nvim-tree").setup()    -- the tree file browser panel
require("telescope").setup()    -- command menu

# TreeSitter

The first slightly complicated one! But much simpler than it was a few years ago. You can see the full list of available TreeSitter parsers here. TreeSitter most obviously improves syntax highlighting. But it also does other more subtle stuff like improve code folding, and enables various other plugins (like treesitter-context) to do their thing. I’ve included a few parsers in the config below, but go to the link above and add as many as you like!

require("nvim-treesitter.configs").setup({
  ensure_installed = {
    "typescript",
    "python",
    "rust",
    "go",
    -- etc!
  },
  sync_install = false,
  auto_install = true,
  highlight = { enable = true, },
})
-- some stuff so code folding uses treesitter instead of older methods
vim.opt.foldmethod = "expr"
vim.opt.foldexpr = "nvim_treesitter#foldexpr()"
vim.opt.foldlevel = 99

# LSP

This is the bit that was basically impossible in Neovim 4 years ago, a huge pain to set up 2 years ago (see my previous blog post), and now pretty straight-forward. All you need is the plugins installed above and these few lines of config. You can see a full list of available servers here. It’s really worth scrolling through and adding whichever ones you like the look of, you might see some you don’t expect!

Note that these require associated language toolchains to be installed, so don’t add eslint if you don’t have npm installed! The installation will fail and you’ll get lots of angry messages in a very small confusing status bar.

require("mason").setup()
require("mason-lspconfig").setup({
  ensure_installed = {
    "gopls",
    "basedpyright",
    "eslint",
    "ruff",
    "rust_analyzer",
    -- etc!
  },
})

You can find the default keybindings for LSP stuff here. Some of the main ones:

# Code formatting

Many languages can be formatted directly by their LSP server (eg ruff for Python), but others still just need an old-school CLI formatter.

For these cases we have conform.nvim. It will try to use the formatters specified, falling back to an LSP if there isn’t one. So you can set up a command to do a conform format (see further down) and it will just work. If you want any linting that your LSP doesn’t provide, there’s also nvim-lint, which does a similar thing but for linting.

require("conform").setup({
  default_format_opts = { lsp_format = "fallback" },
  formatters_by_ft = {
    typescript = { "prettier" },
    typescriptreact = { "prettier" },
    json = { "prettier" },
    -- etc
  },
})

# Autocomplete

We already set up blink.cmp in the plugins, and left it with its default key bindings. You can pretty much just use it, but some useful keys are:

# Key bindings

Everything until this point is pretty universal. If you don’t already have mega-strong vim opinions, just use what I’ve shared above and you’ll probably be quite happy.

Key bindings are obviously quite personal… I’ll share my setup and you can pick and choose what you like.

I already set the leader key to , up at the top. Repeating it here. This is basically a prefix for many commands. Eg you’ll see below that my formatting command is <leader>fo, which means I hit ,fo (one after the other, not at the same time!) to run my formatting command. , is quite a popular choice. \ is the default.

vim.g.mapleader = ","

Then two more customisations that are probably less common. The first lets you hit <space> instead of entering : to enter a command. So eg to open the Lazy dialog to check your installed plugins, you can enter <space>Lazy<Enter>, which is slightly easier than a colon… The second is more particular. The default key for undo in neovim is u, but for redo it’s Ctrl-R, which is horrible. So I map q to redo. So I can hit u q in quick succession to go back and forth.

vim.keymap.set("n", "<space>", ":")
vim.keymap.set("n", "q", "<C-r>")

These basically just set n to always be next search result down the page, and N always up. Same for ' forward when character seeking and ; backwards. The default has these operating relative to the direction you started searching in, which can be hard to keep track of.

vim.keymap.set("n", "n", "v:searchforward ? 'n' : 'N'", { expr = true })
vim.keymap.set("n", "N", "v:searchforward ? 'N' : 'n'", { expr = true })

vim.keymap.set({ "n", "v" }, ";", "getcharsearch().forward ? ',' : ';'", { expr = true })
vim.keymap.set({ "n", "v" }, "'", "getcharsearch().forward ? ';' : ','", { expr = true })

Two little commands for toggling line numbers and word wrapping:

vim.keymap.set("n", "<leader>n", ":set nonumber! relativenumber!<CR>")
vim.keymap.set("n", "<leader>w", ":set wrap! wrap?<CR>")

Moving between and resizing splits. The normal command to e.g. move to the split below is quite tedious. This makes it just Ctrl-j. Similar for the other directions. You can do something similar for resizing if you want… or just use the mouse 😉.

vim.keymap.set("n", "<C-j>", "<C-W><C-J>")
vim.keymap.set("n", "<C-k>", "<C-W><C-K>")
vim.keymap.set("n", "<C-l>", "<C-W><C-L>")
vim.keymap.set("n", "<C-H>", "<C-W><C-H>")

You’ve already installed nvim-tree, now you need some commands to make it work. I find myself using Ctrl-f to open the file browser on the current file repeatedly. Then I hit enter, choose which split to open the file in, and hit Ctrl-c to close the file browser again.

vim.keymap.set("n", "<C-t>", ":NvimTreeFocus<CR>")
vim.keymap.set("n", "<C-f>", ":NvimTreeFindFile<CR>")
vim.keymap.set("n", "<C-c>", ":NvimTreeClose<CR>")

Formatting. This does the conform stuff, falling back to LSP stuff.

vim.keymap.set("n", "<leader>fo", require('conform').format)

Telescope. Don’t skip this one! These are super useful commands that will change how you navigate a codebase. You don’t even need the file browser with these. Hit ,ff then start typing the name of a file. If the file you want isn’t checked in to a repo, ,fa does the same for other files. ,fg gives you instant ripgrep across your files. ,fb lets you quickly switch between recently open buffers (this is what vim people call files). And ,fh is probably the only way you’ll ever get comfortable finding help from within Neovim…

local tele_builtin = require("telescope.builtin")
vim.keymap.set("n", "<leader>ff", tele_builtin.git_files, {})
vim.keymap.set("n", "<leader>fa", tele_builtin.find_files, {})
vim.keymap.set("n", "<leader>fg", tele_builtin.live_grep, {})
vim.keymap.set("n", "<leader>fb", tele_builtin.buffers, {})
vim.keymap.set("n", "<leader>fh", tele_builtin.help_tags, {})

# The full config

Here’s the 180 line Neovim config as a Github Gist.

And here’s my actual current Neovim config, which is a bit longer with one or two extra bits that you probably don’t need.