The final episode: assembling the whole journey from episodes 0-26 into one complete production-grade config architecture — modular init.lua, plugins, LSP, completion, formatting — complete with a daily-driver readiness checklist, best practices, and a recap of the series journey from start to finish.

After discussing testing integration with neotest in episode 26 — how to run and navigate tests without leaving the editor — we arrive at the last episode of the Learn Neovim series. The journey that began in episode 0 with the simple question "what skills and tools should be prepared before learning Neovim?" will now be closed with a much bigger question: "what does a Neovim config deserve to be called production-grade?"
Twenty-seven episodes feel like years ago since we got to know modal editing, wrote our first init.lua, built lazy-loading, LSP, completion, formatting, git integration, debugging, AI, and testing. All those skills have until now felt like separate pieces. Episode 27 is the point where everything is assembled into one living system.
In real work, an engineer does not have "fragmented IDE configs" — what they have is one working environment that must be trustworthy every day, where speed and reliability are the minimum standard. This episode will present a complete production-grade config architecture case study, a daily-driver readiness checklist, and a full recap of the journey from episode 0. More than just code — this episode is about the way of thinking of an engineer who makes their tool an extension of their hand.
A production-grade config starts from structure. Good structure makes a config easy to navigate, easy to debug, and easy to extend. Here is the architecture we built throughout this series, brought together:
nvim/
├── init.lua # Entry point: memuat lazy.nvim & modul-modul
├── .gitignore # Melindungi secret lokal (episode 24)
├── .env.local # Secret lokal (tidak di-commit)
├── stylua.toml # Formatting Lua config (episode 17)
├── lazy-lock.json # Lockfile plugin — reproducibilitas (episode 9)
└── lua/
├── config/
│ ├── env.lua # Deteksi OS & path lintas platform (episode 24)
│ ├── options.lua # Opsi editor (episode 7)
│ ├── keymaps.lua # Leader key & shortcut (episode 7)
│ ├── autocmds.lua # Format-on-save, dll (episode 8)
│ └── lazy.lua # Bootstrap lazy.nvim (episode 9)
└── plugins/
├── core.lua # lazy.nvim + disable plugin bawaan (episode 23)
├── colorscheme.lua # Tema + statusline (episode 10)
├── telescope.lua # Fuzzy finder & navigasi (episode 11)
├── oil.lua # File explorer buffer-based (episode 12)
├── flash.lua # Jump navigation (episode 13)
├── treesitter.lua # Syntax highlighting & parsing (episode 14)
├── lsp.lua # mason + lspconfig + LSP keymaps (episode 15)
├── cmp.lua # Completion + snippets (episode 16)
├── conform.lua # Formatter (episode 17)
├── lint.lua # Linter asinkron (episode 17)
├── gitsigns.lua # Git gutter & blame (episode 18)
├── diffview.lua # Diff & merge conflicts (episode 18)
├── toggleterm.lua # Terminal & lazygit (episode 19)
├── autopairs.lua # Produktivitas editing (episode 20)
├── which-key.lua # Bantuan keymap (episode 20)
├── ai.lua # Copilot/Codeium/Avante (episode 21)
├── dap.lua # Debugging (episode 22)
└── neotest.lua # Testing (episode 26)Every file in lua/plugins/ is a standalone spec — added, removed, or disabled without disturbing the others. This is the power of the modularization we built since episode 9: each plugin lives in its own file with disciplined lazy-loading.
init.lua — A Clean Entry Pointinit.lua should be as small as possible: load the core configuration and bootstrap lazy.nvim, then let the other modules do their work. Never cram big logic here.
-- Entry point: muat modul config inti
require("config.env")
require("config.options")
require("config.keymaps")
require("config.autocmds")
-- Bootstrap lazy.nvim & seluruh plugin (episode 9)
require("config.lazy")lazy.nvim Bootstrap with Reproducibilityconfig/lazy.lua is the heart of it all. Notice how this script is self-bootstrapping — downloading lazy.nvim if it does not exist — so it can run on a new machine without extra manual steps (supporting the automatic bootstrap from episode 24):
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or 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({
spec = {
{ import = "plugins" }, -- auto-import semua lua/plugins/*.lua
},
install = { colorscheme = { "tokyonight" } },
checker = { enabled = true }, -- notifikasi update tersedia
performance = {
rtp = {
disabled_plugins = {
"netrwPlugin", "gzip", "zipPlugin", "tarPlugin", "tohtml", "matchit",
},
},
},
})Tip
spec = { { import = "plugins" } } makes lazy.nvim automatically load all files in lua/plugins/ as specs. Adding a new plugin is just creating one new file — no need to touch init.lua at all. This is why the modular structure from episode 9 keeps being used to this day.
These three modules are the foundation we built in episodes 7 and 8. options.lua controls editor behavior, keymaps.lua controls shortcuts, and autocmds.lua controls automatic reactions to events. Here is the "production-grade" version — complete with the best practices from the entire series:
local env = require("config.env")
-- 1. Tampilan & navigasi (episode 7)
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.cursorline = true
vim.opt.scrolloff = 8
vim.opt.signcolumn = "yes"
vim.opt.termguicolors = true
-- 2. Indentasi (episode 7)
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
vim.opt.smartindent = true
-- 3. Pencarian (episode 7)
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.hlsearch = true
vim.opt.incsearch = true
-- 4. Keandalan & backup (episode 7)
vim.opt.undofile = true
vim.opt.undodir = env.undo_dir
vim.opt.swapfile = false
vim.opt.backup = false
vim.opt.clipboard = "unnamedplus" -- sync dengan sistem clipboard
-- 5. Perilaku modern
vim.opt.mouse = "a"
vim.opt.splitright = true
vim.opt.splitbelow = true
vim.opt.completeopt = "menu,menuone,noselect"
vim.opt.wrap = false
vim.opt.updatetime = 250
vim.opt.timeoutlen = 300
-- 6. Direktori undo dibuat otomatis
vim.fn.mkdir(env.undo_dir, "p")-- Leader key: spasi (episode 7)
vim.g.mapleader = " "
local map = vim.keymap.set
-- Menyimpan & keluar (refleks dasar)
map("n", "<leader>w", "<cmd>w<CR>", { desc = "Save file" })
map("n", "<leader>q", "<cmd>q<CR>", { desc = "Quit window" })
-- Navigasi split (episode 5)
map("n", "<C-h>", "<C-w>h", { desc = "Go to left window" })
map("n", "<C-j>", "<C-w>j", { desc = "Go to lower window" })
map("n", "<C-k>", "<C-w>k", { desc = "Go to upper window" })
map("n", "<C-l>", "<C-w>l", { desc = "Go to right window" })
-- Buffer navigation (episode 5)
map("n", "<Tab>", "<cmd>bnext<CR>", { desc = "Next buffer" })
map("n", "<S-Tab>", "<cmd>bprevious<CR>", { desc = "Previous buffer" })
map("n", "<leader>bd", "<cmd>bdelete<CR>", { desc = "Close buffer" })
-- Mencari (episode 7)
map("n", "<Esc>", "<cmd>noh<CR>", { desc = "Clear search highlight" })
-- Meninggalkan insert mode dengan cepat
map("i", "jk", "<Esc>", { desc = "Exit insert mode" })
map("i", "jj", "<Esc>", { desc = "Exit insert mode" })
-- Jangan menimpa yank saat paste di visual mode (episode 3)
map("v", "p", '"_dP', { desc = "Paste without overwriting register" })local group = vim.api.nvim_create_augroup("user_config", { clear = true })
-- Kembali ke posisi terakhir saat membuka file (episode 8)
vim.api.nvim_create_autocmd("BufReadPost", {
group = group,
callback = function()
local mark = vim.api.nvim_buf_get_mark(0, '"')
if mark[1] > 1 and mark[1] <= vim.api.nvim_buf_line_count(0) then
pcall(vim.api.nvim_win_set_cursor, 0, mark)
end
end,
})
-- Aktifkan wrap hanya untuk file markdown (episode 8)
vim.api.nvim_create_autocmd("FileType", {
group = group,
pattern = { "markdown", "text" },
callback = function()
vim.opt_local.wrap = true
end,
})These three modules are what make Neovim feel like a real IDE. lsp.lua connects Neovim to language servers (episode 15), cmp.lua provides autocompletion (episode 16), and conform.lua handles automatic formatting (episode 17):
{
"neovim/nvim-lspconfig",
event = { "BufReadPre", "BufNewFile" },
dependencies = {
{ "williamboman/mason.nvim", cmd = "Mason", build = ":MasonUpdate" },
"williamboman/mason-lspconfig.nvim",
"hrsh7th/cmp-nvim-lsp",
},
config = function()
require("mason").setup()
require("mason-lspconfig").setup({
ensure_installed = { "gopls", "pyright", "tsserver", "lua_ls", "dockerls" },
})
local capabilities = require("cmp_nvim_lsp").default_capabilities()
local on_attach = function(_, bufnr)
local bufmap = function(keys, fn, desc)
vim.keymap.set("n", keys, fn, { buffer = bufnr, desc = desc })
end
bufmap("gd", vim.lsp.buf.definition, "Go to definition")
bufmap("gr", vim.lsp.buf.references, "References")
bufmap("K", vim.lsp.buf.hover, "Hover documentation")
bufmap("<leader>rn", vim.lsp.buf.rename, "Rename symbol")
bufmap("<leader>ca", vim.lsp.buf.code_action, "Code action")
bufmap("<leader>e", vim.diagnostic.open_float, "Show diagnostics")
end
require("mason-lspconfig").setup_handlers({
function(server)
require("lspconfig")[server].setup({ on_attach = on_attach, capabilities = capabilities })
end,
})
end,
}{
"hrsh7th/nvim-cmp",
event = "InsertEnter",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
"rafamadriz/friendly-snippets",
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
require("luasnip.loaders.from_vscode").lazy_load()
cmp.setup({
snippet = {
expand = function(args) luasnip.lsp_expand(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(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
["<Tab>"] = 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" },
{ name = "path" },
}),
})
end,
}{
"stevearc/conform.nvim",
event = "BufWritePre", -- format saat akan menyimpan
config = function()
require("conform").setup({
formatters_by_ft = {
lua = { "stylua" },
python = { "ruff_fix", "ruff_format" },
go = { "gofmt", "goimports" },
javascript = { "prettierd", "prettier" },
typescript = { "prettierd", "prettier" },
["*"] = { "trim_whitespace" },
},
format_on_save = {
timeout_ms = 1500,
lsp_fallback = true,
},
})
end,
}Note
Notice the lazy-loading patterns on all three: LSP loads at BufReadPre/BufNewFile (the buffer is open before we even get to interact), completion loads at InsertEnter (only needed once you start typing), and the formatter loads at BufWritePre (only needed when about to save). This is the application of the lazy-loading discipline from episode 23 that keeps 50+ plugins from ever blocking startup.
Before a config deserves to be called a daily driver, it must pass this entire checklist. Adapted from the lessons of the whole series:
| No | Area | Item | Episode Reference | Status |
|---|---|---|---|---|
| 1 | Performance | Startup time < 100ms (target < 50ms) | 23 | ☐ |
| 2 | Lazy-loading | All non-essential plugins have a trigger (event/cmd/keys/ft) | 23 | ☐ |
| 3 | Backup | undofile enabled + undo directory created automatically | 7, 24 | ☐ |
| 4 | Security | No secrets in config; .env.local gitignored | 24 | ☐ |
| 5 | Git | gitsigns gutter + blame + diffview for conflicts | 18 | ☐ |
| 6 | Debugging | nvim-dap + nvim-dap-ui + per-language debuggers | 22 | ☐ |
| 7 | Testing | neotest + adapters for the languages you use | 26 | ☐ |
| 8 | AI | Copilot/Codeium/Avante configured (optional) | 21 | ☐ |
| 9 | LSP | mason + lspconfig + keymaps gd/gr/K/<leader>rn | 15 | ☐ |
| 10 | Completion | nvim-cmp + snippets (LuaSnip + friendly-snippets) | 16 | ☐ |
| 11 | Formatting | conform format-on-save for all main languages | 17 | ☐ |
| 12 | Linting | nvim-lint async + diagnostics navigation [d/]d | 17 | ☐ |
| 13 | Navigation | telescope (<leader>ff, <leader>fg) + oil | 11, 12 | ☐ |
| 14 | Terminal | toggleterm + integrated lazygit | 19 | ☐ |
| 15 | Portability | Config works on Linux + macOS + Windows from one repo | 24 | ☐ |
| 16 | Bootstrap | Idempotent install script for new machines | 24 | ☐ |
| 17 | Reproducibility | lazy-lock.json committed | 9 | ☐ |
Important
This checklist is a gate, not an aspiration. A config that has not passed it does not yet deserve to be called production-grade. And remember: the performance and lazy-loading items should be maintained by habit, not just intention — every new plugin must answer the question "when is this plugin actually needed?" from episode 23.
Before the recap, let's summarize the best practices scattered across the series into one list you can hold onto:
| # | Best Practice | Why |
|---|---|---|
| 1 | Always modular: one file per plugin | Easy to debug, easy to disable (episode 9) |
| 2 | Lazy-load everything, without exception | Startup stays under 50ms even with 50+ plugins (episode 23) |
| 3 | Master text objects & motions first, plugins second | A strong foundation is worth more than fancy plugins (episode 3) |
| 4 | Learn :help before asking the internet | Neovim has outstanding documentation; the answer is often there |
| 5 | Build workflows on top of Git | gitsigns, diffview, lazygit — versioned code is safe (episode 18) |
| 6 | Test every config change | If it breaks, Git enables fast rollback (episode 24) |
| 7 | Keep secrets out of the repo | Security is not a feature, but an absolute requirement (episode 24) |
| 8 | Measure before optimizing | --startuptime and :Lazy profile before touching the config (episode 23) |
| 9 | Be consistent, do not change keymaps every week | Muscle memory needs stability (episode 25) |
| 10 | Build your own config | Understanding is an asset you cannot buy from a distro (episode 25) |
Let's look at the big map you have traveled together. Six phases, twenty-eight episodes — from merely getting to know modal editing to building a production-grade work environment:
| Phase | Episodes | Core Material |
|---|---|---|
| Foundation & Modal Editing | 0–4 | Environment setup, Neovim history, modal editing, text objects, registers & macros |
| Windows, Buffer & Lua | 5–9 | Window/buffer management, migration to Lua, options & keymaps, autocmds, lazy.nvim |
| Visualization & Navigation | 10–13 | Colorscheme & statusline, telescope, file explorer, flash & session management |
| Modern IDE Capabilities | 14–18 | Treesitter, LSP & mason, completion, formatting & linting, Git integration |
| Advanced Workflow & AI | 19–22 | Terminal & task management, editing productivity, AI assistant, debugging with DAP |
| Performance, Distribution, Testing | 23–26 | Profiling & advanced lazy-loading, dotfiles & portability, distro vs custom, neotest |
| Production Readiness | 27 | Production-grade config architecture & daily-driver checklist |
From episode 0, still wondering "what are modal editing skills", you can now design a work environment that is fast, reliable, portable, and accountable — complete with LSP, completion, formatting, testing, debugging, and AI, all inside an editor that runs in the terminal. That is a capability few engineers have, and now you have it.
Congratulations — you have completed the Learn Neovim series from episode 0 to episode 27!
Let's take a moment to look back. In episode 27 we assembled everything into one production-grade config architecture: a clean modular structure, a minimal init.lua, a reproducible lazy.nvim bootstrap, solid options/keymaps/autocmds modules, the LSP-completion-formatting trio that turns Neovim into an IDE, as well as the daily-driver readiness checklist and best practices that unite the whole series' lessons.
But the most valuable thing is not the code — it is the way of thinking you now have:
Your journey does not stop here. Some next steps you can take to keep growing:
:help in depth. Neovim is a tool with nearly endless depth; the built-in documentation is your advanced curriculum.Remember the lesson from episode 1: Neovim is an editor with the philosophy of thinking speed in parallel with typing speed. You have traveled 28 episodes to reach the point where the editor no longer blocks you — it becomes an extension of your hand. A skill that never goes out of date: editors will never be replaced, and the way you use yours is a competitive advantage that keeps compounding every day.
Thank you for accompanying this journey to the end. Now — open your terminal, write your own config, and let every keypress become a reflex that brings you closer to the workflow you dream of. Happy coding, and see you in the terminal!