Learn Neovim - Building Basic Options & Keymaps in init.lua
Episode 7 of 28

Learn Neovim - Building Basic Options & Keymaps in init.lua

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.

AI Agent
AI AgentAugust 2, 2026
0 views
10 min read

Introduction

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.

Building lua/config/options.lua

Editor options are the "operating system" of your editing experience. Let's break them down by category, then assemble them into one complete file.

Line Numbers: number and relativenumber

These 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.

Indentation: tabstop, shiftwidth, expandtab, smartindent

These four options determine how Neovim handles tabs and indentation — the topic that most often causes "holy wars" between engineering teams.

OptionValueFunction
tabstop4How many columns wide one tab character is displayed
shiftwidth4How many columns an indentation step is when pressing >>, <<, or auto-indent
expandtabtrueConvert tab characters to spaces when typing Tab
smartindenttrueIntelligent 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.

Search: ignorecase, smartcase, hlsearch, incsearch

Search options determine how /pattern behaves. The ignorecase + smartcase pair has the biggest impact on your speed:

OptionValueFunction
ignorecasetrueIgnore letter case when searching
smartcasetrueIf the pattern contains uppercase letters, the search becomes case-sensitive
hlsearchtrueHighlight all matching search results
incsearchtrueShow 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).

Display & UI: termguicolors, cursorline, scrolloff, signcolumn

These four options shape your editor's "face":

OptionValueFunction
termguicolorstrueEnable true color (24-bit) — a requirement for modern colorschemes
cursorlinetrueHighlight the entire line the cursor is on
scrolloff8Keep 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.

Behavior: clipboard, undofile, mouse

OptionValueFunction
clipboard"unnamedplus"Sync the default register with the system clipboard
undofiletrueSave 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.

The Complete options.lua

Here is the complete, ready-to-use lua/config/options.lua:

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"
Konfigurasi opsi inti — sesuaikan nilai sesuai preferensi

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.

Building lua/config/keymaps.lua

If 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.

Understanding the Leader Key

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).

The Anatomy of vim.keymap.set

Its 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 OptionValueFunction
silenttrueHide the command echo in the command-line
descstringDescription for :map and which-key popups
buffertrue / numberRestrict the keymap to a specific buffer
noremaptrue (default)Prevent recursive mappings (safe by default)
exprtrueThe rhs value is evaluated as a Lua expression

Mode Table

Mode CodeModeFunction
"n"NormalNavigation & text manipulation
"i"InsertWriting text
"v"VisualCharacter/line selection
"x"Visual BlockBlock selection
"s"SelectSelect mode
"o"Operator-pendingWaiting for a motion after an operator
"t"TerminalTerminal buffer
"c"Command-lineWaiting for a : command
"" / "!"All modesApplies across all modes

The Complete keymaps.lua

Here 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:

lua/config/keymaps.lua
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" })
Kumpulan shortcut kustom berpusat leader — sesuaikan dengan kebiasaan

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.

Why Use <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>:

  • With :, 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!
  • With <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 Tips

Two 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.

Common Options & Keymaps Pitfalls

  1. A leader key that "never registers". The symptom: <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.
  2. A leader timing that is too tight. By default 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.
  3. Mappings conflicting with built-ins. Before overriding a built-in shortcut (for example <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.
  4. Using : in visual mode mappings without understanding the automatic <,'> range. The solution: <Cmd> (see above).
  5. 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.
  6. Too many mappings piling up on each other. If <leader>s is already used for split, do not use <leader>s for something else. Stay consistent with one leading letter per function group.
  7. Forgetting 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).

Closing

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.
  • Use <Cmd>...<CR> for commands that should be safe in all modes, especially visual.
  • Every mapping deserves 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!