Neovim From Scratch with Lua
Build a modern Neovim config from scratch using init.lua, lazy.nvim, Treesitter, nvim-lspconfig, and Mason — step by step with no pre-made distribution.
Before you start
- ▸Neovim 0.9 or later installed
- ▸git available in PATH
- ▸A C compiler (gcc or clang) for Treesitter parser compilation
- ▸Node.js and/or Python if installing LSP servers for those languages
Neovim's built-in Lua runtime replaced Vimscript as the preferred configuration language in Neovim 0.5 and has matured substantially since. This guide builds a working Neovim setup from an empty directory: structured init.lua, plugin management with lazy.nvim, syntax highlighting via Treesitter, and language intelligence through nvim-lspconfig and Mason. You will end up with something you understand and can extend, not a pre-packaged distribution.
Prerequisites and Installation
You need Neovim 0.9 or later. Most distro repositories lag behind, so check the version first.
nvim --version
Install or upgrade Neovim
Debian/Ubuntu — the official PPA ships current stable releases:
sudo add-apt-repository ppa:neovim-ppa/stable
sudo apt update
sudo apt install neovim
Fedora/RHEL family — Fedora ships a recent build; on RHEL/Rocky use the COPR:
# Fedora
sudo dnf install neovim
# Rocky Linux / RHEL 9
sudo dnf copr enable agriffis/neovim-nightly
sudo dnf install neovim
Arch:
sudo pacman -S neovim
You also need git, a C compiler (gcc or clang, required by Treesitter), and Node.js or Python if you intend to install LSP servers for those languages. Mason handles most server installation itself, but it shells out to the system npm/pip.
Step 1 — Create the Config Structure
Neovim reads ~/.config/nvim/init.lua as its entry point. A flat single file works but becomes unmanageable quickly. Use this layout instead:
mkdir -p ~/.config/nvim/lua/plugins
touch ~/.config/nvim/init.lua
touch ~/.config/nvim/lua/options.lua
touch ~/.config/nvim/lua/keymaps.lua
The lua/ directory is automatically on Neovim's Lua runtime path, so require("options") loads lua/options.lua. Subdirectories work the same way: require("plugins.treesitter") loads lua/plugins/treesitter.lua.
Populate init.lua
cat > ~/.config/nvim/init.lua <<'EOF'
require("options")
require("keymaps")
require("plugins") -- lazy.nvim bootstrap lives here
EOF
Set core options
cat > ~/.config/nvim/lua/options.lua <<'EOF'
local opt = vim.opt
opt.number = true
opt.relativenumber = true
opt.tabstop = 4
opt.shiftwidth = 4
opt.expandtab = true
opt.smartindent = true
opt.wrap = false
opt.termguicolors = true
opt.signcolumn = "yes"
opt.updatetime = 200
opt.undofile = true
EOF
Step 2 — Bootstrap lazy.nvim
lazy.nvim is the current community standard plugin manager. It supports lazy-loading, lockfiles, and a clean UI. Create lua/plugins/init.lua — this file both installs lazy.nvim if absent and declares all plugins.
cat > ~/.config/nvim/lua/plugins/init.lua <<'EOF'
-- Bootstrap lazy.nvim
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({
{ import = "plugins.treesitter" },
{ import = "plugins.lsp" },
})
EOF
The import directive tells lazy to load plugin specs from separate files, keeping each concern isolated. Open Neovim now — lazy.nvim will clone itself on the first launch.
nvim
Step 3 — Treesitter for Syntax Highlighting
nvim-treesitter replaces regex-based syntax files with compiled parsers. The result is accurate, fast highlighting and enables features like structural text objects.
cat > ~/.config/nvim/lua/plugins/treesitter.lua <<'EOF'
return {
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
event = { "BufReadPost", "BufNewFile" },
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = {
"lua", "python", "javascript", "typescript",
"bash", "json", "yaml", "markdown", "c",
},
highlight = { enable = true },
indent = { enable = true },
auto_install = true,
})
end,
},
}
EOF
The build = ":TSUpdate" field runs the parser compilation step automatically when the plugin is installed or updated. You need gcc or clang present for this to succeed. Parsers are compiled per-language and stored under ~/.local/share/nvim/.
Step 4 — LSP with nvim-lspconfig and Mason
Neovim ships a built-in LSP client; nvim-lspconfig provides ready-made configurations for hundreds of servers. Mason handles downloading and managing the server binaries themselves, so you are not relying on system packages for every language server.
cat > ~/.config/nvim/lua/plugins/lsp.lua <<'EOF'
return {
-- Mason: LSP server installer
{
"williamboman/mason.nvim",
cmd = "Mason",
build = ":MasonUpdate",
config = true, -- calls require("mason").setup() with defaults
},
-- Bridge between Mason and lspconfig
{
"williamboman/mason-lspconfig.nvim",
dependencies = { "williamboman/mason.nvim" },
opts = {
ensure_installed = { "lua_ls", "pyright", "ts_ls" },
automatic_installation = true,
},
},
-- Core LSP configuration
{
"neovim/nvim-lspconfig",
dependencies = { "williamboman/mason-lspconfig.nvim" },
config = function()
local lspconfig = require("lspconfig")
local capabilities = vim.lsp.protocol.make_client_capabilities()
-- Per-server setup
local servers = { "lua_ls", "pyright", "ts_ls" }
for _, server in ipairs(servers) do
lspconfig[server].setup({ capabilities = capabilities })
end
-- Keymaps applied whenever an LSP attaches to a buffer
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(event)
local map = function(keys, func)
vim.keymap.set("n", keys, func, { buffer = event.buf })
end
map("gd", vim.lsp.buf.definition)
map("K", vim.lsp.buf.hover)
map("rn",vim.lsp.buf.rename)
map("ca",vim.lsp.buf.code_action)
map("[d", vim.diagnostic.goto_prev)
map("]d", vim.diagnostic.goto_next)
end,
})
end,
},
}
EOF
The ensure_installed list in mason-lspconfig tells Mason which servers to download automatically. Add or remove server names as needed — run :Mason inside Neovim to browse available servers interactively.
Step 5 — Set a Leader Key
The <leader> key prefixes custom shortcuts. Set it before lazy loads plugins or keymaps that reference it.
cat > ~/.config/nvim/lua/keymaps.lua <<'EOF'
vim.g.mapleader = " " -- Space
vim.g.maplocalleader = " "
-- Quick config reload
vim.keymap.set("n", "sv", "source $MYVIMRC",
{ desc = "Source init.lua" })
EOF
Move the require("keymaps") line to the very top of init.lua so leader is defined before lazy starts loading plugins.
Step 6 — Verify Everything Works
Open Neovim and run the lazy UI:
nvim
# Then inside Neovim:
:Lazy
You should see all plugins listed as installed. Press U in the Lazy UI to update all at once. Then check the LSP:
# Open a Python file and inspect attached servers
:LspInfo
Output will show the active server for the current buffer and root directory detection. Run :Mason to confirm lua_ls, pyright, and ts_ls are installed (green checkmarks).
Test Treesitter with:
:TSInstallInfo
Parsers listed as installed will show a version and path. For any file type, :InspectTree (Neovim 0.9+) opens the parsed syntax tree — a quick proof that Treesitter is active.
Troubleshooting
Treesitter parsers fail to compile
Install a C compiler. On Debian/Ubuntu: sudo apt install build-essential. On Fedora: sudo dnf install gcc. On Arch: sudo pacman -S base-devel. Then run :TSUpdate.
LSP server not attaching
Check :LspInfo for the reason. Common causes: the server is not in ensure_installed, Mason has not finished installing it yet, or the file has no detectable root (no package.json, pyproject.toml, etc.). Open the file from the project root, not a random directory.
Lua type errors from lua_ls about Neovim globals
Add a .luarc.json at the project root or configure lua_ls with the Neovim runtime path:
lspconfig.lua_ls.setup({
settings = {
Lua = {
runtime = { version = "LuaJIT" },
workspace = {
library = vim.api.nvim_get_runtime_file("", true),
checkThirdParty = false,
},
},
},
})
lazy.nvim fails to clone
Confirm git is installed and that github.com is reachable. Behind a corporate proxy, export HTTPS_PROXY before launching Neovim.
Frequently asked questions
- Do I need nvim-cmp for LSP completion to work?
- No. Neovim's built-in completion (triggered with Ctrl-X Ctrl-O) surfaces LSP suggestions without any extra plugin. nvim-cmp adds a richer popup UI and multiple completion sources, but it is optional.
- What is the difference between Mason and nvim-lspconfig?
- nvim-lspconfig provides the configuration templates that tell Neovim how to launch and communicate with each language server. Mason handles downloading the actual server binaries. They solve different problems and are usually used together.
- Can I use this config alongside an existing init.vim?
- No, not directly. Neovim loads either init.vim or init.lua, not both. If you have an existing init.vim, migrate its settings to Lua or source it from init.lua using vim.cmd('source ~/.config/nvim/legacy.vim') as a temporary bridge.
- Why does Treesitter highlighting sometimes look worse than the old regex highlighting?
- The parser may not be installed for that file type, or the colorscheme lacks Treesitter highlight groups. Run :TSInstall <filetype> and switch to a Treesitter-aware theme such as tokyonight or catppuccin.
- Is lazy.nvim's lockfile safe to commit to version control?
- Yes, and it is recommended. The lazy-lock.json file records the exact commit hash of every plugin, letting you reproduce the same setup on another machine or roll back a bad plugin update with :Lazy restore.
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.