Inilah momen peralihan dari "pemakai" menjadi "pembangun": pahami struktur direktori konfigurasi modern, kuasai dasar bahasa Lua, dan kenali Neovim Lua API (vim.opt, vim.g, vim.keymap.set, vim.api) beserta interoperabilitas dengan Vimscript lewat vim.fn dan vim.cmd.

Setelah di episode 5 sebelumnya kita membahas cara mengelola buffer, window, dan tab — mulai dari split window, navigasi antar buffer, hingga menutup buffer tanpa merusak layout — kalian sekarang sudah bisa bekerja dengan banyak file sekaligus secara efisien. Tapi ada perasaan yang pasti muncul setelah beberapa hari memakai Neovim: "saya ingin membuat editor ini benar-benar milik saya."
Mungkin kalian ingin mengganti default seperti :set number, menambah shortcut yang terasa alami, atau menyimpan pengaturan favorit agar tidak perlu mengetik ulang setiap kali. Di sinilah kalian mulai menulis konfigurasi. Dan di episode kali ini, kita akan menempuh jalur yang tidak mungkin dihindari oleh siapa pun yang serius dengan Neovim: meninggalkan Vimscript dan beralih sepenuhnya ke Lua.
Mengapa ini penting di dunia nyata? Karena Neovim sejak versi 0.5 menjadikan Lua sebagai first-class citizen — bahasa pemrograman nyata yang di-embed langsung ke dalam editor. Sebagian besar ekosistem plugin modern (Telescope, Treesitter, LSP, lazy.nvim) ditulis dalam Lua, dan distro populer seperti LazyVim atau NvChad 100% berbasis Lua. Menguasai konfigurasi Lua berarti kalian bisa membaca dan memodifikasi hampir semua setup Neovim yang ada di dunia — dari repo dotfiles engineer senior sampai config default sebuah distro. Ini adalah keterampilan yang langsung berdampak pada pekerjaan kalian sebagai software engineer atau DevOps.
Sebelum menulis baris kode pertama, mari kita pahami mengapa Neovim memilih Lua dan bukan Vimscript.
Vimscript (Vim's scripting language) sudah berusia puluhan tahun dan memiliki beberapa kelemahan struktural: sintaksnya unik dan terasa "asing", array-nya mulai dari indeks 1, data structures-nya terbatas, performanya lambat untuk logika kompleks, dan debugging-nya menyakitkan. Vimscript seperti belajar bahasa kedua yang hanya berguna di satu tempat.
Lua, di sisi lain, adalah bahasa scripting berusia 30 tahun yang terkenal ringan, cepat, dan mudah di-embed — ia dipakai di game (World of Warcraft, Roblox) dan sistem embedded. Runtime Lua hanya beberapa ratus kilobyte, namun ekspresif dan familiar bagi programmer mana pun. Neovim memilih Lua 5.1 (kompatibel dengan LuaJIT) dan mengintegrasikannya begitu dalam sehingga seluruh API editor — buffer, window, options, keymaps, LSP, Treesitter — bisa diakses langsung dari Lua.
| Aspek | Vimscript | Lua |
|---|---|---|
| Performa | Lambat untuk logika kompleks | Sangat cepat (LuaJIT) |
| Struktur data | Terbatas | Table (array + dict dalam satu tipe) |
| Sintaks | Unik, terasa asing | Familiar, mirip bahasa modern |
| Ekosistem plugin modern | Legacy | Standar de-facto |
| Debugging | Sulit | Lebih mudah (error message jelas) |
| Modularization | source file | require dengan namespace |
Note
Vimscript tidak hilang dan masih didukung penuh — bahkan kita akan belajar cara memanggil fungsi Vimscript dari Lua di episode ini. Namun arah pengembangan Neovim jelas: semua fitur dan plugin baru ditulis dalam Lua, dan karier konfigurasi kalian jauh lebih ringan jika langsung memulai dengan Lua.
Semua konfigurasi Neovim berada di direktori ~/.config/nvim/ di Linux/macOS (di Windows: %localappdata%\nvim). Direktori ini berada di dalam runtimepath Neovim, artinya file-file di dalamnya otomatis dikenali oleh Neovim saat startup.
File utama adalah init.lua — pengganti modern dari init.vim. Namun config yang rapi tidak menaruh semua isi di satu file. Arsitektur yang disepakati komunitas adalah struktur modular seperti ini:
~/.config/nvim/
├── init.lua # Entry point utama
├── lazy-lock.json # Lockfile plugin (episode 9)
└── lua/
├── config/ # Konfigurasi inti milik kita sendiri
│ ├── options.lua # Pengaturan opsi editor
│ ├── keymaps.lua # Shortcut kustom
│ ├── autocmds.lua # Autocommand (episode 8)
│ └── lazy.lua # Setup plugin manager
└── plugins/ # Spec plugin, satu file per plugin
├── telescope.lua
├── treesitter.lua
└── lsp.luaKeajaiban utama ada di folder lua/. Folder ini otomatis ditambahkan ke package.path Neovim, sehingga setiap file di dalamnya bisa dimuat dengan fungsi require. Ini memberi kita cara yang bersih untuk memecah konfigurasi menjadi modul-modul kecil yang masing-masing punya tanggung jawab tunggal — persis seperti prinsip modularisasi di codebase production.
Jangan khawatir jika kalian belum pernah menyentuh Lua — bahasanya kecil dan bisa dikuasai dalam 30 menit. Mari kita bedah tipe data dan konstruksi dasarnya.
Lua hanya punya delapan tipe dasar, dan untuk konfigurasi Neovim kalian cukup tahu enam:
| Tipe | Contoh | Keterangan |
|---|---|---|
nil | local x = nil | "Tidak ada nilai"; menghapus variabel |
boolean | true, false | Kebenaran logika |
number | 42, 3.14 | Semua angka, tanpa beda int/float |
string | "halo", 'halo' | Teks |
table | {} | Satu-satunya struktur data (array & dict) |
function | function() end | Nilai yang bisa dipanggil (first-class) |
localVariabel dalam Lua bersifat global secara default. Untuk menghindari polusi namespace, konvensi kuatnya adalah selalu mendeklarasikan dengan local:
local editor = "neovim" -- lokal, hanya berlaku di file ini
global_variable = "oops" -- global, mencemari _G — hindari!Table adalah jantung Lua. Ia bisa berperilaku sebagai array dan dictionary sekaligus. Satu hal yang wajib diingat: indeks array Lua mulai dari 1, bukan 0.
-- Array (list)
local hobbies = { "neovim", "docker", "k8s" }
print(hobbies[1]) -- "neovim" (indeks mulai dari 1!)
print(#hobbies) -- 3 (operator # = panjang array)
-- Dictionary (key-value)
local config = {
leader = " ",
verbose = 0,
name = "Arman",
}
print(config.leader) -- akses dengan titik
print(config["name"]) -- atau dengan indeks stringFungsi dalam Lua adalah nilai biasa — bisa disimpan di variabel, dikirim sebagai argumen, dan dikembalikan dari fungsi lain. Ini penting karena banyak API Neovim menerima callback (fungsi yang dipanggil saat suatu kejadian terjadi):
local function tambah(a, b)
return a + b
end
-- Fungsi anonim (tanpa nama) — dipakai untuk callback
local cb = function()
print("dipanggil!")
endTip
Konvensi yang harus kalian ingat saat membaca config orang lain: sebuah file Lua yang di-require akan dieksekusi, dan nilai return terakhir dari file tersebut yang menjadi hasil require. Itulah sebabnya file modul konfigurasi biasanya mengakhiri file dengan return { ... } — sebuah pola yang akan sangat sering kita pakai untuk spec plugin di episode 9.
vim.*)Setelah menguasai dasar Lua, inilah bagian yang paling berharga: namespace vim.* yang membuka seluruh kemampuan editor. Berikut peta namespace yang paling sering dipakai:
| Namespace | Fungsi | Contoh |
|---|---|---|
vim.opt | Atur opsi editor (gaya :set) | vim.opt.number = true |
vim.o | Atur opsi global secara langsung | vim.o.tabstop = 4 |
vim.opt_local | Atur opsi khusus buffer/window | vim.opt_local.spell = true |
vim.g | Variabel global (gaya let g:) | vim.g.mapleader = " " |
vim.env | Variabel environment | vim.env.HOME |
vim.keymap.set | Definisikan shortcut | vim.keymap.set("n", "<leader>w", ":w<CR>") |
vim.api | API tingkat rendah Neovim (nvim_*) | vim.api.nvim_create_user_command(...) |
vim.fn | Panggil fungsi Vimscript | vim.fn.expand("%:p") |
vim.cmd | Jalankan perintah Ex (:command) | vim.cmd("set number") |
vim.lsp | Integrasi Language Server Protocol | vim.lsp.start(...) |
vim.opt: Mengatur Opsi Editorvim.opt mengembalikan sebuah objek yang memungkinkan kita mengatur opsi dengan cara yang mirip :set. Ada perbedaan penting: vim.opt mendukung semantik lengkap :set (termasuk operasi +=, -=, ^=) lewat metode seperti :append() dan :remove():
vim.opt.number = true -- set number
vim.opt.relativenumber = true -- set relativenumber
vim.opt.tabstop = 4 -- set tabstop=4
vim.opt.expandtab = true -- set expandtab
vim.opt.ignorecase = false -- set noignorecase
-- Opsi berbentuk daftar (list) — pakai :append / :remove
vim.opt.path:append("**") -- set path+=**
vim.opt.wildignore:append({ "node_modules", ".git" })vim.g: Variabel GlobalIni padanan Lua dari let g:name = value di Vimscript. Contoh paling terkenal adalah leader key yang akan kita pelajari detail di episode 7:
vim.g.mapleader = " " -- set leader key jadi spasi
vim.g.maplocalleader = " " -- leader lokal buffer
vim.g.my_plugin_enabled = true -- contoh variabel untuk pluginvim.api: API Tingkat Rendahvim.api berisi ratusan fungsi nvim_* yang menjadi jembatan antara Lua dan kernel Neovim. Kita akan memakai beberapa di antaranya di episode 8 (autocommand & user command), tapi contoh singkat berikut menunjukkan interaksi langsung dengan buffer dan window:
local buf = vim.api.nvim_get_current_buf() -- buffer aktif
local name = vim.api.nvim_buf_get_name(buf) -- path absolut buffer
local lines = vim.api.nvim_buf_get_lines(0, 0, 5, false) -- baris 1-5
vim.api.nvim_win_set_cursor(0, { 10, 0 }) -- pindah kursor ke baris 10Note
Parameter 0 pada banyak fungsi nvim_* berarti "buffer/window aktif" — ini singkatan yang sangat umum sehingga kalian harus mengenalnya. vim.api.nvim_get_current_buf() bisa ditulis vim.api.nvim_get_current_buf(0)? Tidak — fungsi tersebut tidak menerima argumen. Tapi untuk fungsi seperti nvim_buf_get_name(buf), mengirim 0 berarti "buffer aktif saat ini".
vim.fn dan vim.cmd)Kalian tidak perlu meninggalkan Vimscript sepenuhnya — kadang ada fungsi yang hanya tersedia di Vimscript. Neovim menyediakan dua jembatan utama.
vim.fn: Memanggil Fungsi VimscriptSemua fungsi bawaan Vimscript (fungsi :help functions) bisa dipanggil dari Lua lewat vim.fn.<nama_fungsi>(...). Nama fungsi ditulis dengan underscore, bukan huruf besar:
local filename = vim.fn.expand("%:t") -- nama file aktif
local cwd = vim.fn.getcwd() -- direktori kerja
local has_file = vim.fn.filereadable("Makefile") -- 1 atau 0 (perhatikan!)
local lines = vim.fn.line("$") -- jumlah baris bufferCaution
Kesalahan umum #1: menganggap vim.fn{:lua} selalu mengembalikan true/false. Banyak fungsi Vimscript mengembalikan 1 atau 0 (bukan boolean). Di Lua, 0 itu truthy — artinya if vim.fn.filereadable("x") then akan bernilai benar meskipun file-nya tidak ada! Bandingkan dengan == 1 atau gunakan fungsi yang memang mengembalikan boolean. Ini bug yang paling sering menyergap pemula yang migrasi dari Vimscript.
vim.cmd: Menjalankan Perintah ExUntuk menjalankan perintah command-line (:set, :e, dll) dari Lua, gunakan vim.cmd:
vim.cmd("set number")
vim.cmd(":vsplit") -- tanda titik dua boleh, tidak wajib
vim.cmd("silent! %s/foo/bar/g")
-- String multiline dengan kurung siku ganda (long string)
vim.cmd([[
set number
set relativenumber
set tabstop=4
]])Untuk kepentingan kompatibilitas, Neovim juga menyediakan arah sebaliknya. Ini berguna saat kita masih punya baris Vimscript lama di init.vim selama masa transisi:
:lua print("halo dari lua")
:lua vim.opt.number = true
:lua require("config.options") -- require modul
:lua <<EOF
local x = 42
print(x)
EOF
:luafile ~/.config/nvim/lua/config/options.luainit.vim → init.luaSaatnya menggabungkan semuanya. Berikut perbandingan sebelum-sesudah dari konfigurasi paling umum. Perhatikan pola migrasinya: set → vim.opt, let → vim.g, nnoremap → vim.keymap.set.
set number
set relativenumber
set tabstop=4 shiftwidth=4 expandtab
set mouse=a
let mapleader = " "
nnoremap <leader>w :w<CR>
set clipboard=unnamedplus
Tabel konversi cepat yang akan sangat sering kalian pakai:
| Vimscript | Lua |
|---|---|
set number | vim.opt.number = true |
set noignorecase | vim.opt.ignorecase = false |
set path+=** | vim.opt.path:append("**") |
setlocal spell | vim.opt_local.spell = true |
let g:mapleader = " " | vim.g.mapleader = " " |
nnoremap x y | vim.keymap.set("n", "x", "y") |
set nu + set rnu | vim.opt.number, vim.opt.relativenumber = true, true |
requireKeindahan struktur lua/config/ muncul saat kita mengaktifkannya dari init.lua. Fungsi require memetakan path dengan aturan sederhana: titik (.) menggantikan garis miring (/) dan ekstensi .lua tidak ditulis.
require("config.options") -- memuat lua/config/options.lua
require("config.keymaps") -- memuat lua/config/keymaps.lua
require("config.autocmds") -- memuat lua/config/autocmds.lua
-- atau, jika kita ingin mengekspos variabel dari modul:
local opts = require("config.options")Important
Kesalahan umum #2: salah menulis path require{:lua}. Perhatikan tiga aturan: gunakan titik (config.options), bukan garis miring (config/options); jangan tulis ekstensi (.lua); dan pastikan file berada di dalam folder lua/ yang sama dengan file yang memanggilnya. Error yang muncul biasanya berbunyi module 'config.options' not found — begitu kalian melihat error ini, artinya path require tidak cocok dengan lokasi file.
Selain dua jebakan vim.fn dan require di atas, berikut kesalahan lain yang paling sering ditemui:
vim.opt tapi mengharapkan perilaku vim.o. vim.opt.number = true mengembalikan objek opsi; kalian harus mengakses nilainya (vim.opt.number:get()) jika ingin membaca kembali. Untuk membaca cepat, gunakan vim.o.number.hobbies[0] akan bernilai nil di Lua. Selalu mulai dari 1.local sehingga variabel menjadi global dan menabrak variabel plugin. Jadikan local sebagai refleks.vim.g.mapleader setelah keymap menggunakan <leader>. Nilai leader dibaca saat keymap didefinisikan. Jika <leader> belum terisi, keymap akan terikat ke tombol yang salah. (Kita bahas detailnya di episode 7.)set noX bukan vim.opt.X = false? Ya, itu benar — tapi beberapa opsi (misalnya compatible) tidak punya pasangan boolean langsung. Cek :help options bila ragu.init.vim terlalu cepat. Selama migrasi, Neovim membaca init.lua terlebih dahulu dan hanya membaca init.vim jika init.lua tidak ada. Pindahkan konfigurasi bertahap per blok, lalu hapus init.vim saat semuanya berjalan.vim.fn.getcwd (tanpa ()) mengembalikan fungsi itu sendiri, bukan hasilnya. vim.fn.getcwd() yang benar.Tip
Cara tercepat untuk menguji baris Lua tanpa me-restart Neovim: ketik :lua vim.opt.number = true langsung di command-line, atau tekan : lalu ketik lua untuk membuka prompt Lua interaktif. Untuk memuat ulang file config, :luafile % akan mengeksekusi file yang sedang terbuka. Ini mempercepat siklus coba-coba secara drastis.
Di episode 6 ini kita telah membangun fondasi konfigurasi Neovim modern. Kalian memahami struktur direktori ~/.config/nvim/ dengan init.lua sebagai entry point dan folder lua/ sebagai rumah modul. Kalian menguasai dasar Lua — tipe data, variabel dengan local, table (array & dictionary), fungsi, dan modularization dengan require. Terakhir, kalian mengenal peta Neovim Lua API: vim.opt, vim.g, vim.keymap.set, vim.api, serta jembatan ke Vimscript lewat vim.fn dan vim.cmd.
Poin penting yang harus kalian bawa:
vim.fn dan vim.cmd.local.require("config.options") memuat lua/config/options.lua — titik menggantikan garis miring, tanpa ekstensi.vim.fn yang mengembalikan 1/0 butuh pembanding eksplisit dengan == 1, karena 0 bersifat truthy di Lua.vim.g.mapleader harus di-set sebelum keymap yang memakai <leader>.Sekarang kalian sudah siap membangun config sungguhan. Di episode 7 selanjutnya, kita akan langsung praktik: membangun Options & Keymaps dasar di init.lua — menyusun lua/config/options.lua dan lua/config/keymaps.lua yang lengkap, memahami leader key, dan membuat shortcut kustom yang akan kalian pakai setiap hari. Pastikan tetap semangat!