My 2025 Neovim Configuration
A synopsis of my Neovim configuration in 2025
I’m being deadset serious. It’s improved beyond this. I’ll post a 2026 update.
I have recently been performing neovim configurations on fresh re-wipes using ChatGPT generated configurations. This has, for the most part, been exceptionally simple however it has recently started hallucinating.
I did dip my toes into lazyvim however the preconfigurations were too excessive and learning curve frustrating. I much prefer a simpler implementation and I really don’t understand the modern internets obsessive need to implement emojis into literally everything. So on the nth rewipe of the week, I’m going to do this the ‘old’ way of manually figuring it out and documenting what I did.
Neovim Installation
I’ve recently switched over to Ubuntu. I was applying for a job at Canonical and realized that I switched to Debian/Arch exclusively nearly ten years ago, so I needed to see what was up. This co-incided with getting a ‘gaming’ laptop which is my first Intel+Nvidia combination in a very long time. Surprisingly, Ubuntus Nvidia drivers fixed a lot of issues I’ve had with my Samsung Odyssey G9 under Linux, and despite some occassional glitches coming out of hibernation, the drivers have been rock solid. Unfortunately, Ubuntu ships 0.9.5 but Neovim is up to release 0.11.1; and plugins require the ‘uv’ library which is 0.10.0+.
Installing Neovim from Github is the only real solution without dipping into third party repos. Neovims installation guide is the best source of truth, however the steps that I followed are below:
curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz
sudo rm -rf /opt/nvim
sudo tar -C /opt -xzf nvim-linux-x86_64.tar.gz
Alias Neovim
I have bad muscle memories from damn near decades of habit. Vim/Vi are handy muscle memories to have because I’m constantly chopping between vi, vim and nvim between multiple installations/distributions/clients/etc.
Personally I like to alias vi and vim to nvim on my personal to avoid my backspace key from wearing out.
Add the following to your ~/.bashrc
alias vi=nvim
alias vim=nvim
Then source it with . ~/.bashrc or however you want to refresh your shell.
Plugin Manager
I have been a long term fan of vim-plug. It’s fast and simple, but requires re-opening neovim after making changes to the plugin list. I recently trialed out lazy.nvim and I quite liked the way that it functioned, even against something as robust as vim-plug.
vim-plug does feature a more minimalist installation however lazy can be installed pretty simply.
You need to create two directories:
mkdir -p ~/.config/nvim/lua/{config,plugins}
Your lazy configuration will sit in lua/config/lazy.lua:
-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).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 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
vim.opt.rtp:prepend(lazypath)
-- Make sure to setup `mapleader` and `maplocalleader` before
-- loading lazy.nvim so that mappings are correct.
-- This is also a good place to setup other settings (vim.opt)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
-- Setup lazy.nvim
require("lazy").setup({
spec = {
-- import your plugins
{ import = "plugins" },
},
-- Configure any other settings here. See the documentation for more details.
-- colorscheme that will be used when installing plugins.
install = { colorscheme = { "habamax" } },
-- automatically check for plugin updates
checker = { enabled = true },
})
Under the ...setup() function, you’ll see it importing from the word ‘plugins’. This import points lazy.nvim towards ~/.config/nvim/plugins, which is where you’ll be configuring each plugin.
To ensure that config/lazy.lua is loaded when Neovim starts, configure your ~/.config/nvim/init.lua to pull it it with the following block:
require("config.lazy")
Yes, it’s like Python.
The above will fail if you don’t have any plugin specs, but I imagine that noone installs a plugin manager without wanting to install a plugin…
File Tree
I dipped my toes into TreeSitter previous and went to all the effort of getting icons and such working. Between setting it up myself and using the preconfigured version in LazyGit, I just don’t see the need for all of the wasted space.
NerdTree is fast, simple and logical. TreeSitter key bindings are a little more intuitive to guess, but I ran into some pain points switching between relative and absolute filepaths and it just doesn’t ‘vibe’ with how I’ll open massive file trees and jump between multiple projects. There’s probably fixes, but it’s easier just to use what I know.
To install NerdTree using Lazy, you’ll need to provide a lua spec for Lazy to use. In ~/.config/nvim/lua/plugins/nerdtree.lua, add the following:
return {
"preservim/nerdtree",
keys = {
{ "<C-t>", "<cmd>NERDTreeToggle<CR>", desc = "Toggle NERDTree" },
},
config = function()
vim.g.NERDTreeShowHidden=1
end,
}
The above snippet also includes some configurations and key bindings that provide a solid basis for adaptation later.
When you open Neovim, Lazy will open a window that runs through the installation and will automatically install NerdTree (capital I/i to install). In my case, it showed nerdtree under “Not Loaded”. This is because my configuration was originally invalid (above worked for me)
The above configuration will tell Lazy to install preservim/nerdtree, configure Ctrl+t to toggle NERDTree and have NerdTree show hidden files (like .gitlab-ci.yml) by default. I don’t know why I associate Ctrl+t to toggling NerdTree, but that’s just my muscle memory. <leader>n makes no sense to me personally, but then again I get confused when people remove B as a command in vim.
CMP / Code Completion
I find a lot of the code completion engines a bit painful to use and it’s something that ChatGPT has struggled to figure out for me. I have found some success in manually configuring CMP, which I like primarily because it doesn’t insist on using it’s recommendation the moment that I hit Enter, meaning it stays out of my way unlike codium and other code completion modules.
Annoyingly though, the following plugin spec was found to work using ChatGPT (converting the installation config from nvim-cmps git repo):
return {
-- LSP config core
{ "neovim/nvim-lspconfig" },
-- Completion engine
{ "hrsh7th/nvim-cmp" },
-- Completion sources
{ "hrsh7th/cmp-nvim-lsp" },
{ "hrsh7th/cmp-buffer" },
{ "hrsh7th/cmp-path" },
{ "hrsh7th/cmp-cmdline" },
-- Snippet engine and snippet completion source (vsnip here)
{ "hrsh7th/vim-vsnip" },
{ "hrsh7th/cmp-vsnip" },
-- Setup nvim-cmp and lspconfig after loading plugins
{
"hrsh7th/nvim-cmp",
event = "InsertEnter", -- lazy-load on entering insert mode
dependencies = {
"neovim/nvim-lspconfig",
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"hrsh7th/cmp-cmdline",
"hrsh7th/cmp-vsnip",
"hrsh7th/vim-vsnip",
},
config = function()
local cmp = require("cmp")
cmp.setup({
snippet = {
expand = function(args)
vim.fn["vsnip#anonymous"](args.body)
end,
},
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 }),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "vsnip" },
}, {
{ name = "buffer" },
}),
})
-- Use buffer source for '/' and '?'
cmp.setup.cmdline({ "/", "?" }, {
mapping = cmp.mapping.preset.cmdline(),
sources = {
{ name = "buffer" },
},
})
-- Use cmdline & path source for ':'
cmp.setup.cmdline(":", {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({
{ name = "path" },
}, {
{ name = "cmdline" },
}),
matching = { disallow_symbol_nonprefix_matching = false },
})
-- Setup lspconfig with cmp capabilities
local capabilities = require("cmp_nvim_lsp").default_capabilities()
local lspconfig = require("lspconfig")
-- Replace <YOUR_LSP_SERVER> with actual LSP servers you want to start
-- Example: "pyright", "tsserver", "clangd", etc.
local servers = {
"pyright", -- Python
"gopls", -- Go
"intelephense", -- PHP
"cssls", -- CSS
"html", -- HTML
"marksman", -- Markdown
"yamlls", -- YAML
"jsonls", -- JSON
"tsserver", -- JavaScript / TypeScript
"scssls", -- SCSS / SASS
"bashls", -- Bash / Shell
"sqlls", -- SQL (generic)
}
for _, server in ipairs(servers) do
lspconfig[server].setup({
capabilities = capabilities,
})
end
end,
},
}
If you grep out <YOUR_LSP_SERVER>, you’ll see where you can configure your LSP servers. You can find a list here however I have left mine ‘factory’ while including some language servers for what I code in.
I want to get a feel for the above configuration before making some of my own decisions based on experience. I’ll add an addendum to this post if changes are found after publishing.
How do you install these LSP servers?
Well installing these would require a combination of apt and npm usage. You should probably just accept that using Lazy/CMP will require NPM if you’re going to be leveraging LSP servers (makes avoiding COC for avoiding NPM dependency a bit stupid now, right?), so we’ll use Mason to make our lives easier.
Yes, your system bloats out with this stuff but it does make dev a lot easier.
Mason
This will manage your LSP server requirements.
The plugin spec for Mason as defined in ~/.config/nvim/lua/plugins/mason.lua:
{
{
"williamboman/mason.nvim",
build = ":MasonUpdate",
config = true,
},
{
"williamboman/mason-lspconfig.nvim",
dependencies = { "williamboman/mason.nvim" },
opts = {
ensure_installed = {
"pyright",
"gopls",
"intelephense",
"html",
"cssls",
"jsonls",
"yamlls",
"bashls",
"marksman",
"tsserver",
"sqlls",
},
},
},
{
"neovim/nvim-lspconfig",
dependencies = {
"williamboman/mason.nvim",
"williamboman/mason-lspconfig.nvim",
},
config = function()
local lspconfig = require("lspconfig")
local capabilities = require("cmp_nvim_lsp").default_capabilities()
local servers = {
"pyright",
"gopls",
"intelephense",
"html",
"cssls",
"jsonls",
"yamlls",
"bashls",
"marksman",
"tsserver",
"sqlls",
}
for _, server in ipairs(servers) do
lspconfig[server].setup({
capabilities = capabilities,
})
end
end,
},
}
The above carries over the LSP servers that we defined for CMP.
You may need to massage the configuration. Once you have it working, running :Mason will auto sync all of the configured LSP servers to ensrue that they are available.
Floatterm / LazyGit
Float term is a super useful plugin. I like to use it with LazyGit to save dropping out of Neovim to commit changes.
Floaterm is my go-to for this however ChatGPT recommended ToggleTerm which piqued my interest, because I did grow accustomed to the built-in terminal in Codium during my short stint using that. So I’m going to install ToggleTerm and see if I like it, but I might revert back to Floaterm in the future.
Annoyingly, the LTS version of Ubuntu doesn’t ship lazygit. Less annoyingly, it’s out of date anyway so I’ll just install it manually for now and deal with this when it breaks:
LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | \grep -Po '"tag_name": *"v\K[^"]*')
curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz"
tar xf lazygit.tar.gz lazygit
sudo install lazygit -D -t /usr/local/bin/
The plugin spec in ~/.config/lua/plugins/toggleterm.lua
return {
{
"akinsho/toggleterm.nvim",
version = "*",
config = function()
local toggleterm = require("toggleterm")
toggleterm.setup({
open_mapping = [[<c-\>]],
direction = "horizontal",
size = 15,
})
-- Create a floating terminal for lazygit
local Terminal = require("toggleterm.terminal").Terminal
local lazygit = Terminal:new({
cmd = "lazygit",
direction = "float",
hidden = true,
})
function _G._lazygit_toggle()
lazygit:toggle()
end
-- Map <leader>g to toggle lazygit floating terminal
vim.api.nvim_set_keymap("n", "<leader>g", "<cmd>lua _G._lazygit_toggle()<CR>", { noremap = true, silent = true })
end,
},
}
I would have really struggled configuring these without GPT to handle that heavy lifting and help keep me from having to learn each plugins nuances individually. I got this far within the hour, so you’ll excuse me for generating code snippets as needed sorry.
Note
I’m just leaving this here. I’ve spent a little longer than desired configuring this, testing this and documenting this. Inside of about 3 hours with documentation, testing, experimentation, etc.
I’m going to put this down and come back later. I actually sat down to design up a website lol.
Lessons Learned
Some lessons that I’ve learned that might help some other newbies to Lua/Neovim
- The
configandplugindirectories must sit under~/.config/nvim/luaotherwise Neovim fails to find these files. This didn’t cause issues when sitting in ~/.config/nvim while making changes, but traversing away caused things not to load correctly. Confused the crap out of me.