Time to build a real config: write lua/config/options.lua to define the editor's behavior and feel, then lua/config/keymaps.lua with custom shortcuts centered around the leader key — the first two pillars of a Neovim that is truly yours.

After covering the modern configuration directory structure, Lua language basics, and the Neovim Lua API like vim.opt and vim.keymap.set in episode 6, now it is time for something more satisfying: building a real configuration you will use every day.
Picture this: every engineer using a modern IDE has a distinctive editor "feel" — line numbers on the left, consistent indentation, smart search, and shortcuts that feel like extensions of their hands. When you switch machines, change work laptops, or move to a new server, all that "feel" usually disappears and has to be set up again. With a neatly stored Neovim config, all your preferences follow you anywhere — and this is where options (behavior settings) and keymaps (custom shortcuts) become the first two pillars you must build.
In this episode, we will write two complete, ready-to-use files: lua/config/options.lua and lua/config/keymaps.lua. For every option, I will explain why it matters — not just what it does — so you can decide for yourself which ones fit your working style, instead of simply copying someone else's config.
lua/config/options.luaEditor options are the "operating system" of your editing experience. Let's break them down by category, then assemble them into one complete file.
number and relativenumberThese two options determine how line numbers are displayed. number shows the absolute line number in the left gutter. relativenumber shows the relative distance from the cursor position — the lines above and below the cursor show 1, 2, 3, and so on, while the cursor's own line shows its absolute number.
Note
Why is relativenumber so beloved? Because vertical navigation in Neovim is often done with counts: 5j to move down five lines, 3k to move up three lines. With relative numbers, you can see the distance directly on screen and type the right count without guessing. This makes moving between lines as fast as reading a number. You can enable both at once: number to give absolute position context, relativenumber to guide movement.
tabstop, shiftwidth, expandtab, smartindentThese four options determine how Neovim handles tabs and indentation — the topic that most often causes "holy wars" between engineering teams.
| Option | Value | Function |
|---|---|---|
tabstop | 4 | How many columns wide one tab character is displayed |
shiftwidth | 4 | How many columns an indentation step is when pressing >>, <<, or auto-indent |
expandtab | true | Convert tab characters to spaces when typing Tab |
smartindent | true | Intelligent auto-indentation that follows code blocks |
Important
The difference between tabstop and shiftwidth is often confused: tabstop controls the display of tabs already in the file, while shiftwidth controls the step size of the indentation you create. The modern practice (especially for Go, Python, JavaScript) is expandtab so files only contain spaces — this avoids tabs looking different in other editors. For specific languages that require tabs (e.g. Makefiles), you can disable expandtab per-filetype — we discuss this in episode 8.
ignorecase, smartcase, hlsearch, incsearchSearch options determine how /pattern behaves. The ignorecase + smartcase pair has the biggest impact on your speed:
| Option | Value | Function |
|---|---|---|
ignorecase | true | Ignore letter case when searching |
smartcase | true | If the pattern contains uppercase letters, the search becomes case-sensitive |
hlsearch | true | Highlight all matching search results |
incsearch | true | Show matches live as you type |
Tip
The ignorecase + smartcase combination is one of the biggest "quality of life" features in Vim. With both enabled, typing /config matches config, Config, and CONFIG. But the moment you type an uppercase letter, for example /Config, the search automatically becomes case-sensitive. The result: you almost never have to think about letter case when searching — exactly like the search behavior in modern IDEs. One side effect you should know: after a search, hlsearch leaves the highlight behind. Get in the habit of clearing it with :nohlsearch or Ctrl+L (or make a keymap for it — see the keymaps section later).
termguicolors, cursorline, scrolloff, signcolumnThese four options shape your editor's "face":
| Option | Value | Function |
|---|---|---|
termguicolors | true | Enable true color (24-bit) — a requirement for modern colorschemes |
cursorline | true | Highlight the entire line the cursor is on |
scrolloff | 8 | Keep at least 8 lines of context above/below the cursor when scrolling |
signcolumn | "yes" | Always show the sign column on the left |
Note
scrolloff may sound trivial, but its value is large: without scrolloff, the cursor can stop at the very top/bottom line of the screen, making you lose context around the code. With scrolloff = 8, the cursor stops eight lines before the screen edge, so the surrounding code stays visible — very helpful when reading long functions. Meanwhile, signcolumn = "yes" makes the left column (where LSP error icons and git indicators appear) always present, so the code does not "jump" left-right every time an icon appears or disappears.
clipboard, undofile, mouse| Option | Value | Function |
|---|---|---|
clipboard | "unnamedplus" | Sync the default register with the system clipboard |
undofile | true | Save undo history to disk — undo persists after restart |
mouse | "a" | Enable mouse support in all modes |
Note
clipboard = "unnamedplus" is a game changer on Linux. Without this option, copying in Neovim (y) is not available in other apps — the "+ register is separate from the system clipboard. With unnamedplus, the default " register is pointed at the clipboard, so y, p, d interact directly with the clipboard. The trade-off: overwriting text with p or d also changes the system clipboard contents. undofile is an unsung hero: every change is saved to a .undo file, so you can undo edits even after restarting Neovim — including mistakes made a week ago.
options.luaHere is the complete, ready-to-use lua/config/options.lua:
local opt = vim.opt
-- Nomor baris
opt.number = true
opt.relativenumber = true
-- Indentasi
opt.tabstop = 4
opt.shiftwidth = 4
opt.expandtab = true
opt.smartindent = true
-- Pencarian
opt.ignorecase = true
opt.smartcase = true
opt.hlsearch = true
opt.incsearch = true
-- Tampilan & UI
opt.termguicolors = true
opt.cursorline = true
opt.scrolloff = 8
opt.signcolumn = "yes"
-- Perilaku
opt.clipboard = "unnamedplus"
opt.undofile = true
opt.mouse = "a"Tip
You do not need to memorize all options. The best way to learn them: run :help options for the full list, or :help <option-name> for one option's explanation. Once your config is active, check actual values with :set <option>? — for example :set scrolloff?. This small habit turns you from a config copier into a config understander.
lua/config/keymaps.luaIf options define the editor's behavior, keymaps define your speed. This is the most personal place in the entire config — and the part most often rewritten as you discover your own work patterns.
The leader key concept is one of Vim's most important inventions. The idea is simple: provide one special "trigger" key so all your custom shortcuts live under that namespace, without colliding with built-in shortcuts.
vim.g.mapleader = " " sets space as the leader. Consequently, the combination <leader>w means pressing space then w. Why space? Because the space key in normal mode has no important function and is always easily reachable with both thumbs — that is why space has become the de-facto standard in the modern Neovim community.
Important
The golden rule: vim.g.mapleader must be set BEFORE all keymaps that use <leader>. The leader value is read when the keymap is defined. If a keymap is created before mapleader is set, <leader> falls back to the default value (backslash) — and your shortcuts suddenly do not work as expected. Therefore, put the line vim.g.mapleader = " " at the very top of keymaps.lua (or in the file loaded earliest).
vim.keymap.setIts full signature: vim.keymap.set(mode, lhs, rhs, opts).
mode — the mode where the keymap applies (see the mode table below).lhs — the key combination you press (left-hand side).rhs — the action to run (right-hand side): can be a command string or a Lua function.opts — advanced options like silent, desc, buffer, noremap.opts Option | Value | Function |
|---|---|---|
silent | true | Hide the command echo in the command-line |
desc | string | Description for :map and which-key popups |
buffer | true / number | Restrict the keymap to a specific buffer |
noremap | true (default) | Prevent recursive mappings (safe by default) |
expr | true | The rhs value is evaluated as a Lua expression |
| Mode Code | Mode | Function |
|---|---|---|
"n" | Normal | Navigation & text manipulation |
"i" | Insert | Writing text |
"v" | Visual | Character/line selection |
"x" | Visual Block | Block selection |
"s" | Select | Select mode |
"o" | Operator-pending | Waiting for a motion after an operator |
"t" | Terminal | Terminal buffer |
"c" | Command-line | Waiting for a : command |
"" / "!" | All modes | Applies across all modes |
keymaps.luaHere is the complete lua/config/keymaps.lua. Notice that we create the alias local map = vim.keymap.set to keep the file concise and readable:
vim.g.mapleader = " "
vim.g.maplocalleader = " "
local map = vim.keymap.set
-- ==================== Normal Mode ====================
-- Simpan & keluar
map("n", "<leader>w", "<Cmd>w<CR>", { desc = "Simpan file" })
map("n", "<leader>q", "<Cmd>q<CR>", { desc = "Tutup window" })
map("n", "<leader>x", "<Cmd>bd<CR>", { desc = "Tutup buffer" })
-- Navigasi buffer
map("n", "<leader>bn", "<Cmd>bnext<CR>", { desc = "Buffer berikutnya" })
map("n", "<leader>bp", "<Cmd>bprevious<CR>", { desc = "Buffer sebelumnya" })
map("n", "<leader>bb", "<Cmd>b#<CR>", { desc = "Buffer alternatif" })
-- Window split
map("n", "<leader>sh", "<Cmd>split<CR>", { desc = "Split horizontal" })
map("n", "<leader>sv", "<Cmd>vsplit<CR>", { desc = "Split vertikal" })
map("n", "<leader>so", "<Cmd>only<CR>", { desc = "Tutup window lain" })
-- Pindah antar window (Ctrl-w h/j/k/l tanpa Ctrl-w)
map("n", "<C-h>", "<C-w>h", { desc = "Ke window kiri" })
map("n", "<C-j>", "<C-w>j", { desc = "Ke window bawah" })
map("n", "<C-k>", "<C-w>k", { desc = "Ke window atas" })
map("n", "<C-l>", "<C-w>l", { desc = "Ke window kanan" })
-- Hapus sorotan pencarian
map("n", "<Esc><Esc>", "<Cmd>nohlsearch<CR>", { desc = "Hapus sorotan" })
map("n", "<leader>nh", "<Cmd>nohlsearch<CR>", { desc = "Hapus sorotan" })
-- File explorer bawaan (akan diganti plugin di episode 12)
map("n", "<leader>e", "<Cmd>Ex<CR>", { desc = "Buka file explorer" })
-- ==================== Insert Mode ====================
-- Keluar insert mode dengan jj (lebih cepat dari Esc)
map("i", "jj", "<Esc>", { desc = "Keluar insert mode" })
map("i", "<C-s>", "<Cmd>w<CR>", { desc = "Simpan tanpa keluar mode" })
-- ==================== Visual Mode ====================
-- Pertahankan seleksi saat indentasi
map("v", "<", "<gv", { desc = "Indentasi kiri (pertahankan seleksi)" })
map("v", ">", ">gv", { desc = "Indentasi kanan (pertahankan seleksi)" })
map("v", "p", '"_dP', { desc = "Paste tanpa menimpa register" })
-- ==================== Terminal Mode ====================
map("t", "<Esc><Esc>", "<C-\\><C-n>", { desc = "Keluar terminal ke normal" })Tip
Notice the repeating pattern: almost every normal mode shortcut starts with <leader> followed by one letter that groups the function (b = buffer, s = split, w = write). This is the leader-key "namespace" — with one leading letter, you can intuitively guess the function of other mappings. As shortcuts grow (episode 9+), this pattern keeps the config organized.
<Cmd> and Not :?This is one of those details that is rarely explained but has a big impact. Most tutorials write mappings like map("n", "<leader>w", ":w<CR>"). But there is a subtle difference between : and <Cmd>:
:, Neovim enters command-line mode and then types the command. The result: the cursor temporarily moves, an echo appears in the command-line, and in visual mode, a range <,'> is automatically prepended to the command — so :w in visual mode will only write the selected lines!<Cmd>...<CR>, the command executes without leaving the active mode and without echo. This is why the mappings above use <Cmd>w<CR> — cleaner, and safe to use in visual mode.Caution
Common mistake: copying mappings from old tutorials that use :w<CR>, then being confused when the command behaves strangely in visual mode (for example only writing part of the file). Use <Cmd>...<CR> for commands you want to run intact in any mode.
silent and desc TipsTwo small options with a big impact:
silent = true hides the command echo in the command-line. Without it, every <leader>w shows :w at the bottom of the screen — a small visual distraction that gets annoying over time.desc gives every mapping a name. It appears in :map for debugging and — more importantly — becomes the main material for the which-key popup we will install in episode 20. Describing mappings from the start is a small investment with a big return.<leader>w throws E15: Invalid expression or does nothing. The cause is almost always mapleader set after the keymap — or the config being reloaded before mapleader had a chance to be set.timeoutlen is 1000ms. If you press space then wait more than a second before the next letter, the mapping will not trigger. If it feels slow, lower it with opt.timeoutlen = 500 in options.lua.<C-h> for windows), first check :map <C-h> to see its original function. Sometimes changing a standard key unknowingly breaks a feature you are using.: in visual mode mappings without understanding the automatic <,'> range. The solution: <Cmd> (see above).expandtab enabled on all files can break files that require tabs (Makefiles). Solution: disable it per-filetype — we will discuss this with ftplugin in episode 8.<leader>s is already used for split, do not use <leader>s for something else. Stay consistent with one leading letter per function group.silent. Mappings that show echo feel "noisy". Get in the habit of including { silent = true, desc = ... } from the start.Note
After writing or changing a config file, do not forget to reload it so the changes take effect: run :luafile ~/.config/nvim/lua/config/options.lua (for one file) or restart Neovim. In episode 9, lazy.nvim will provide a more convenient way to reload config (including plugin-related keymaps).
In episode 7 we built the first two pillars of a Neovim config. In lua/config/options.lua, you set line numbers with number + relativenumber, indentation with tabstop/shiftwidth/expandtab/smartindent, smart search with ignorecase + smartcase, display with termguicolors/cursorline/scrolloff/signcolumn, and behavior with clipboard = "unnamedplus", undofile, and mouse. In lua/config/keymaps.lua, you set space as the leader key, created grouped shortcuts (<leader>w, <leader>b, <leader>s), learned the difference between <Cmd> and :, and realized why silent and desc matter.
Key points to take with you:
vim.g.mapleader = " " must be placed before all <leader> keymaps.scrolloff = 8 and signcolumn = "yes" keep context and prevent layout jumping.clipboard = "unnamedplus" connects the default register to the system clipboard.<Cmd>...<CR> for commands that should be safe in all modes, especially visual.silent = true and desc for comfort and readability.Your config is now alive: the editor feels comfortable, and your hands are starting to "dance" over the leader key. But there is one type of power we have not built yet — the editor's ability to react to events on its own: auto-saving, tidying whitespace, or running special commands. In episode 8, we will cover Autocommands, Filetype Detection & Custom Commands — building lua/config/autocmds.lua, creating custom commands like :TrimWhitespace, and setting per-language options with ftplugin. Stay motivated!