Neovim from Scratch: A Modern Lua-Based Configuration Guide

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

BindingAction
SpaceLeader key
<leader>ffFind files (Telescope)
<leader>fgLive grep (search in files)
<leader>fbList open buffers
<leader>eToggle file tree
gdGo to definition (LSP)
grGo to references (LSP)
KHover documentation (LSP)
<leader>caCode actions (LSP)
<leader>rnRename symbol (LSP)
<leader>wSave file
<leader>qQuit
Ctrl+h/j/k/lNavigate windows

Starter Configurations

If building from scratch feels overwhelming, use a pre-configured distribution:

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:

Start with the basics, add plugins as you need them, and invest time in learning the keybindings — Neovim rewards the effort.