A Vim Config From Scratch
Build a Neovim config from scratch: sensible defaults, lazy.nvim plugin manager, Treesitter, LSP via Mason, and autocompletion — all explained step by step.
Before you start
- ▸Neovim 0.9 or newer installed
- ▸git available in PATH for plugin cloning
- ▸A terminal emulator with 24-bit colour support (e.g. kitty, alacritty, GNOME Terminal 3.36+)
- ▸Basic familiarity with Vim modal editing (Normal, Insert, Command modes)
A well-tuned Vim (or Neovim) config makes the difference between a capable editor and one that actively fights you. This guide builds a configuration from an empty file: sensible defaults first, then a plugin manager, language server support, and a handful of plugins that pull their weight. Commands target Neovim (the actively developed fork) but the Vim-compatible sections are clearly marked.
Neovim vs Classic Vim
Neovim uses ~/.config/nvim/init.vim (Vimscript) or ~/.config/nvim/init.lua (Lua). Lua is now the preferred language for Neovim configs — it is faster, composable, and all modern plugins expect it. Classic Vim uses ~/.vimrc. This guide uses Lua + Neovim for the main path, with Vimscript equivalents noted where they differ.
Step 1: Install Neovim
Distro packages lag behind. Install at least Neovim 0.9 for full LSP and Treesitter support.
Debian / Ubuntu
sudo add-apt-repository ppa:neovim-ppa/stable
sudo apt update && sudo apt install neovim
Fedora / RHEL 9+
sudo dnf install neovim
Arch
sudo pacman -S neovim
Verify the version:
nvim --version
You should see NVIM v0.9.x or newer. Anything below 0.8 will break several plugins in this guide.
Step 2: Create the Config Directory
mkdir -p ~/.config/nvim
touch ~/.config/nvim/init.lua
All your configuration lives under ~/.config/nvim/. Lua files in ~/.config/nvim/lua/ are auto-discoverable via require(). A clean layout to start with:
~/.config/nvim/
├── init.lua # entry point
└── lua/
├── options.lua # editor settings
├── keymaps.lua # key bindings
└── plugins.lua # plugin manager bootstrap
Step 3: Sensible Defaults
Open ~/.config/nvim/lua/options.lua and add the following. Each line is commented so you know what you are enabling and why.
nvim ~/.config/nvim/lua/options.lua
local opt = vim.opt
opt.number = true -- absolute line numbers
opt.relativenumber = true -- relative numbers for fast jumps
opt.expandtab = true -- spaces, not tabs
opt.shiftwidth = 4 -- indent width
opt.tabstop = 4
opt.smartindent = true
opt.wrap = false -- no line wrapping
opt.ignorecase = true -- case-insensitive search
opt.smartcase = true -- ...unless you type uppercase
opt.cursorline = true -- highlight current line
opt.termguicolors = true -- full 24-bit colour
opt.splitright = true -- vertical splits go right
opt.splitbelow = true -- horizontal splits go below
opt.scrolloff = 8 -- keep 8 lines visible above/below cursor
opt.signcolumn = "yes" -- always show sign column (avoids jitter)
opt.updatetime = 250 -- faster CursorHold (helps LSP)
opt.clipboard = "unnamedplus" -- share system clipboard
Wire this file into init.lua:
echo 'require("options")' >> ~/.config/nvim/init.lua
Step 4: Install lazy.nvim (Plugin Manager)
lazy.nvim is the current standard for Neovim. It loads plugins on demand, shows a clean UI, and supports lockfiles for reproducible installs. vim-plug remains a solid choice for classic Vim or for anyone preferring Vimscript — a note on it follows this section.
Add the bootstrap snippet to ~/.config/nvim/lua/plugins.lua. This downloads lazy.nvim automatically if it is not already present:
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 go here
})
Register it in init.lua:
echo 'require("plugins")' >> ~/.config/nvim/init.lua
vim-plug Alternative (Vim / Vimscript users)
curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
Then in ~/.vimrc: wrap plugins between call plug#begin() and call plug#end(), then run :PlugInstall.
Step 5: Add Core Plugins
Replace the empty require("lazy").setup({ }) call with a practical plugin list:
require("lazy").setup({
-- Colour scheme
{ "folke/tokyonight.nvim", priority = 1000,
config = function() vim.cmd.colorscheme("tokyonight") end },
-- Syntax highlighting via Treesitter
{ "nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = { "lua", "python", "bash", "c", "rust", "javascript" },
highlight = { enable = true },
indent = { enable = true },
})
end },
-- Fuzzy finder
{ "nvim-telescope/telescope.nvim",
dependencies = { "nvim-lua/plenary.nvim" } },
-- File tree
{ "nvim-tree/nvim-tree.lua",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function() require("nvim-tree").setup() end },
-- Status line
{ "nvim-lualine/lualine.nvim",
config = function() require("lualine").setup() end },
-- LSP config + Mason (installs language servers)
{ "neovim/nvim-lspconfig" },
{ "williamboman/mason.nvim",
build = ":MasonUpdate",
config = function() require("mason").setup() end },
{ "williamboman/mason-lspconfig.nvim" },
-- Autocompletion
{ "hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
}},
})
Launch Neovim. lazy.nvim will clone and install everything automatically:
nvim
Step 6: Configure LSP
Mason installs language server binaries; mason-lspconfig bridges Mason with nvim-lspconfig. Add a new file ~/.config/nvim/lua/lsp.lua:
local lspconfig = require("lspconfig")
local mason_lsp = require("mason-lspconfig")
local capabilities = require("cmp_nvim_lsp").default_capabilities()
mason_lsp.setup({
ensure_installed = { "pyright", "lua_ls", "bashls", "rust_analyzer" },
automatic_installation = true,
})
mason_lsp.setup_handlers({
function(server_name)
lspconfig[server_name].setup({ capabilities = capabilities })
end,
})
-- Key bindings applied only when an LSP attaches
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(ev)
local opts = { buffer = ev.buf }
vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
vim.keymap.set("n", "rn", vim.lsp.buf.rename, opts)
vim.keymap.set("n", "ca", vim.lsp.buf.code_action, opts)
vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, opts)
vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts)
end,
})
echo 'require("lsp")' >> ~/.config/nvim/init.lua
Open any Python or Lua file and you should see diagnostics appearing in the sign column. Run :Mason to open the Mason UI and install additional servers interactively.
Step 7: Autocompletion Setup
Add ~/.config/nvim/lua/completion.lua:
local cmp = require("cmp")
local luasnip = require("luasnip")
cmp.setup({
snippet = {
expand = function(args) luasnip.lsp_expand(args.body) end,
},
mapping = cmp.mapping.preset.insert({
[""] = cmp.mapping.complete(),
[""] = cmp.mapping.confirm({ select = true }),
[""] = cmp.mapping(function(fallback)
if cmp.visible() then cmp.select_next_item()
elseif luasnip.expand_or_jumpable() then luasnip.expand_or_jump()
else fallback() end
end, { "i", "s" }),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "buffer" },
}),
})
echo 'require("completion")' >> ~/.config/nvim/init.lua
Verification
- Run
:checkhealthinside Neovim. Review any ERROR lines — warnings for optional features are normal. - Open a Python file. Hover over a symbol and press
K. A documentation popup should appear. - Press
:Telescope find_filesto confirm fuzzy finding works. - Run
:Lazyto see the plugin manager dashboard confirming all plugins loaded.
Troubleshooting
- LSP not attaching: Run
:LspInfoin a relevant buffer. If it shows no client, check:Masonto confirm the server installed, and verify the file type with:set filetype?. - No colour / broken colours in terminal: Ensure your terminal supports 24-bit colour. For tmux users, add
set -g default-terminal "tmux-256color"andset -as terminal-features ",xterm-256color:RGB"to~/.tmux.conf. - lazy.nvim fails to bootstrap: Confirm
gitis installed and you have internet access. Corporate proxies require settinghttps_proxybefore launching Neovim. - Slow startup: Run
:Lazy profileto see per-plugin load times. Move heavy plugins toevent = "VeryLazy"or specific file-type triggers.
Frequently asked questions
- Can I use this config with classic Vim instead of Neovim?
- The options and key-binding sections translate directly to ~/.vimrc, but lazy.nvim, native LSP, and Treesitter are Neovim-only. Use vim-plug plus coc.nvim for a comparable Vim experience.
- How do I add support for a new programming language?
- Open :Mason and install the relevant language server, then add its name to the ensure_installed list in lsp.lua. Add the language to Treesitter's ensure_installed list for syntax highlighting.
- Is init.lua interchangeable with init.vim?
- No. Neovim loads whichever file exists; if both exist, init.lua takes precedence in Neovim 0.9+. You can call vim.cmd() inside Lua to run any Vimscript snippet you need.
- Why does my colour scheme look wrong inside tmux or SSH?
- Both tmux and some SSH clients strip 24-bit colour escape codes. Set termguicolors to true in options.lua and configure your terminal emulator and tmux to advertise RGB colour support.
- How do I keep plugins updated?
- Run :Lazy update from inside Neovim. lazy.nvim also maintains a lazy-lock.json lockfile you can commit to version control for reproducible installs across machines.
Related guides
Bash Arrays and Associative Arrays
Master bash indexed and associative arrays: declaration, element access, looping, mapfile, namerefs, and practical patterns for real scripting work.
Bash Functions and Variable Scoping
Master Bash function scoping with local variables, source-based libraries, correct use of return codes, and array passing techniques including namerefs.
Bash Loops: for, while and until
Learn all three Bash loop types — for, while, and until — with practical, copy-paste examples covering file iteration, counting, polling, and safe line reading.
Bash Scripting for Beginners
Learn Bash scripting from scratch: shebang lines, variables, conditionals, loops, and arguments, plus a real backup script to tie it all together.