Neovim from Scratch: A Modern Lua-Based Configuration Guide
Neovim is a modern fork of Vim that adds Lua scripting, built-in LSP support, and Treesitter for syntax highlighting. It’s fast, extensible, and — with the right configuration — can rival any IDE while running entirely in the terminal.
This guide walks through setting up Neovim from scratch with Lua, installing plugins with lazy.nvim, configuring LSP for code completion, and building a productive development environment.
Why Neovim?
- Speed: Starts instantly, handles large files without lag
- Lua configuration: Modern scripting language instead of Vimscript
- Built-in LSP: Native Language Server Protocol support for code intelligence
- Treesitter: Fast, incremental syntax highlighting and code parsing
- Terminal-native: Works over SSH, in tmux, on any system
- Extensible: 10,000+ plugins available
Installing Neovim
macOS
brew install neovim
Ubuntu/Debian
# Latest stable via PPA
sudo add-apt-repository ppa:neovim-ppa/stable
sudo apt update
sudo apt install neovim
Arch Linux
sudo pacman -S neovim
AppImage (Any Linux)
curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim.appimage
chmod u+x nvim.appimage
sudo mv nvim.appimage /usr/local/bin/nvim
Verify:
nvim --version
Configuration Directory Structure
Neovim uses ~/.config/nvim/ for configuration:
~/.config/nvim/
├── init.lua # Entry point
├── lua/
│ ├── config/
│ │ ├── options.lua # Editor options
│ │ ├── keymaps.lua # Key mappings
│ │ └── autocmds.lua # Auto commands
│ └── plugins/
│ ├── init.lua # Plugin manager setup
│ ├── lsp.lua # LSP configuration
│ ├── treesitter.lua
│ ├── telescope.lua
│ └── ui.lua # Theme, statusline, etc.
└── lazy-lock.json # Plugin lock file
Basic Options (init.lua)
Create ~/.config/nvim/init.lua:
-- Load core configuration
require("config.options")
require("config.keymaps")
require("config.autocmds")
-- Load plugin manager
require("plugins")
Create ~/.config/nvim/lua/config/options.lua:
local opt = vim.opt
-- Line numbers
opt.number = true
opt.relativenumber = true
-- Indentation
opt.tabstop = 2
opt.shiftwidth = 2
opt.expandtab = true
opt.smartindent = true
-- Search
opt.ignorecase = true
opt.smartcase = true
opt.hlsearch = false
opt.incsearch = true
-- Appearance
opt.termguicolors = true
opt.signcolumn = "yes"
opt.cursorline = true
opt.scrolloff = 8
-- Behavior
opt.wrap = false
opt.swapfile = false
opt.backup = false
opt.undofile = true
opt.undodir = vim.fn.stdpath("data") .. "/undo"
opt.clipboard = "unnamedplus"
opt.mouse = "a"
opt.splitright = true
opt.splitbelow = true
opt.updatetime = 250
opt.timeoutlen = 300
-- Set leader key
vim.g.mapleader = " "
vim.g.maplocalleader = " "
Key Mappings
Create ~/.config/nvim/lua/config/keymaps.lua:
local map = vim.keymap.set
-- Better window navigation
map("n", "<C-h>", "<C-w>h", { desc = "Move to left window" })
map("n", "<C-j>", "<C-w>j", { desc = "Move to lower window" })
map("n", "<C-k>", "<C-w>k", { desc = "Move to upper window" })
map("n", "<C-l>", "<C-w>l", { desc = "Move to right window" })
-- Resize windows
map("n", "<C-Up>", ":resize +2<CR>", { desc = "Increase window height" })
map("n", "<C-Down>", ":resize -2<CR>", { desc = "Decrease window height" })
map("n", "<C-Left>", ":vertical resize -2<CR>", { desc = "Decrease window width" })
map("n", "<C-Right>", ":vertical resize +2<CR>", { desc = "Increase window width" })
-- Move lines up/down
map("v", "J", ":m '>+1<CR>gv=gv", { desc = "Move selection down" })
map("v", "K", ":m '<-2<CR>gv=gv", { desc = "Move selection up" })
-- Better indenting
map("v", "<", "<gv", { desc = "Indent left" })
map("v", ">", ">gv", { desc = "Indent right" })
-- Buffer navigation
map("n", "<leader>bn", ":bnext<CR>", { desc = "Next buffer" })
map("n", "<leader>bp", ":bprevious<CR>", { desc = "Previous buffer" })
map("n", "<leader>bd", ":bdelete<CR>", { desc = "Close buffer" })
-- Save and quit
map("n", "<leader>w", ":w<CR>", { desc = "Save" })
map("n", "<leader>q", ":q<CR>", { desc = "Quit" })
-- Clear search highlighting
map("n", "<Esc>", ":noh<CR>", { desc = "Clear search highlight" })
-- File explorer
map("n", "<leader>e", ":Netrw<CR>", { desc = "File explorer" })
Plugin Manager: lazy.nvim
lazy.nvim is the most popular Neovim plugin manager — fast, with lazy loading and a UI.
Create ~/.config/nvim/lua/plugins/init.lua:
-- 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)
-- Load plugins
require("lazy").setup({
-- Import plugin specs from lua/plugins/
{ import = "plugins.ui" },
{ import = "plugins.lsp" },
{ import = "plugins.treesitter" },
{ import = "plugins.telescope" },
})
Essential Plugins
Theme and UI
Create ~/.config/nvim/lua/plugins/ui.lua:
return {
-- Colorscheme
{
"folke/tokyonight.nvim",
lazy = false,
priority = 1000,
config = function()
vim.cmd.colorscheme("tokyonight-night")
end,
},
-- Status line
{
"nvim-lualine/lualine.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("lualine").setup({
options = { theme = "tokyonight" },
})
end,
},
-- File explorer
{
"nvim-tree/nvim-tree.lua",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("nvim-tree").setup()
vim.keymap.set("n", "<leader>e", ":NvimTreeToggle<CR>", { desc = "Toggle file tree" })
end,
},
-- Indent guides
{
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
config = function()
require("ibl").setup()
end,
},
-- Git signs in gutter
{
"lewis6991/gitsigns.nvim",
config = function()
require("gitsigns").setup()
end,
},
-- Which-key (shows available keybindings)
{
"folke/which-key.nvim",
config = function()
require("which-key").setup()
end,
},
}
Telescope (Fuzzy Finder)
Create ~/.config/nvim/lua/plugins/telescope.lua:
return {
{
"nvim-telescope/telescope.nvim",
branch = "0.1.x",
dependencies = {
"nvim-lua/plenary.nvim",
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
},
config = function()
local telescope = require("telescope")
telescope.setup({
defaults = {
file_ignore_patterns = { "node_modules", ".git/" },
},
})
telescope.load_extension("fzf")
local builtin = require("telescope.builtin")
vim.keymap.set("n", "<leader>ff", builtin.find_files, { desc = "Find files" })
vim.keymap.set("n", "<leader>fg", builtin.live_grep, { desc = "Live grep" })
vim.keymap.set("n", "<leader>fb", builtin.buffers, { desc = "Buffers" })
vim.keymap.set("n", "<leader>fh", builtin.help_tags, { desc = "Help tags" })
vim.keymap.set("n", "<leader>fr", builtin.oldfiles, { desc = "Recent files" })
end,
},
}
Treesitter (Syntax Highlighting)
Create ~/.config/nvim/lua/plugins/treesitter.lua:
return {
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = {
"lua", "javascript", "typescript", "python", "rust",
"go", "html", "css", "json", "yaml", "toml",
"bash", "markdown", "markdown_inline", "vim", "vimdoc",
},
highlight = { enable = true },
indent = { enable = true },
auto_install = true,
})
end,
},
}
LSP (Language Server Protocol)
Create ~/.config/nvim/lua/plugins/lsp.lua:
return {
-- LSP installer
{
"williamboman/mason.nvim",
config = function()
require("mason").setup()
end,
},
{
"williamboman/mason-lspconfig.nvim",
dependencies = { "williamboman/mason.nvim" },
config = function()
require("mason-lspconfig").setup({
ensure_installed = {
"lua_ls", -- Lua
"ts_ls", -- TypeScript/JavaScript
"pyright", -- Python
"rust_analyzer", -- Rust
"gopls", -- Go
"html", -- HTML
"cssls", -- CSS
"jsonls", -- JSON
},
})
end,
},
-- LSP configuration
{
"neovim/nvim-lspconfig",
dependencies = {
"williamboman/mason-lspconfig.nvim",
"hrsh7th/cmp-nvim-lsp",
},
config = function()
local lspconfig = require("lspconfig")
local capabilities = require("cmp_nvim_lsp").default_capabilities()
-- Setup each server
local servers = { "lua_ls", "ts_ls", "pyright", "rust_analyzer", "gopls", "html", "cssls", "jsonls" }
for _, server in ipairs(servers) do
lspconfig[server].setup({ capabilities = capabilities })
end
-- LSP keymaps (set when LSP attaches to a buffer)
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(event)
local map = function(keys, func, desc)
vim.keymap.set("n", keys, func, { buffer = event.buf, desc = desc })
end
map("gd", vim.lsp.buf.definition, "Go to definition")
map("gr", vim.lsp.buf.references, "Go to references")
map("K", vim.lsp.buf.hover, "Hover documentation")
map("<leader>ca", vim.lsp.buf.code_action, "Code action")
map("<leader>rn", vim.lsp.buf.rename, "Rename symbol")
map("<leader>D", vim.lsp.buf.type_definition, "Type definition")
end,
})
end,
},
-- Autocompletion
{
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
},
config = function()
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({
["<C-Space>"] = cmp.mapping.complete(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
["<Tab>"] = cmp.mapping.select_next_item(),
["<S-Tab>"] = cmp.mapping.select_prev_item(),
["<C-d>"] = cmp.mapping.scroll_docs(4),
["<C-u>"] = cmp.mapping.scroll_docs(-4),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "buffer" },
{ name = "path" },
}),
})
end,
},
}
Essential Key Bindings Reference
| Binding | Action |
|---|---|
Space | Leader key |
<leader>ff | Find files (Telescope) |
<leader>fg | Live grep (search in files) |
<leader>fb | List open buffers |
<leader>e | Toggle file tree |
gd | Go to definition (LSP) |
gr | Go to references (LSP) |
K | Hover documentation (LSP) |
<leader>ca | Code actions (LSP) |
<leader>rn | Rename symbol (LSP) |
<leader>w | Save file |
<leader>q | Quit |
Ctrl+h/j/k/l | Navigate windows |
Starter Configurations
If building from scratch feels overwhelming, use a pre-configured distribution:
- LazyVim: https://www.lazyvim.org — Opinionated, well-maintained
- NvChad: https://nvchad.com — Fast, beautiful defaults
- AstroNvim: https://astronvim.com — Feature-rich with a community
Install LazyVim:
# Backup existing config
mv ~/.config/nvim ~/.config/nvim.backup
# Clone LazyVim starter
git clone https://github.com/LazyVim/starter ~/.config/nvim
# Start Neovim (plugins install automatically)
nvim
Summary
Neovim with Lua configuration provides a fast, powerful editing experience. The combination of lazy.nvim for plugin management, Treesitter for highlighting, Mason for LSP installation, and Telescope for navigation creates a complete IDE-like environment in the terminal.
Key resources:
- Neovim: https://neovim.io
- lazy.nvim: https://github.com/folke/lazy.nvim
- Mason: https://github.com/williamboman/mason.nvim
- LazyVim: https://www.lazyvim.org
- Awesome Neovim: https://github.com/rockerBOO/awesome-neovim
Start with the basics, add plugins as you need them, and invest time in learning the keybindings — Neovim rewards the effort.