Buat Neovim bekerja sendiri: pelajari autocommand yang merespons kejadian editor (save otomatis, trim whitespace, highlight yank), buat perintah kustom seperti :TrimWhitespace dengan nvim_create_user_command, dan sesuaikan perilaku per bahasa dengan filetype detection dan modeline.

Setelah di episode 7 sebelumnya kita membangun options.lua dan keymaps.lua — dua pilar pertama config Neovim — kalian sekarang memiliki editor yang terasa nyaman dan shortcut yang responsif. Tapi perhatikan satu hal: semua yang kita bangun sejauh ini menunggu perintah dari kalian. Kalian yang menekan tombol, kalian yang menjalankan perintah. Belum ada satu pun perilaku yang berjalan sendiri.
Di dunia nyata, developer tidak bekerja seperti itu. Kita ingin editor bereaksi terhadap apa yang terjadi: rapi otomatis saat menyimpan file, menghapus trailing whitespace tanpa diminta, mengembalikan posisi kursor saat membuka file yang sama, dan menampilkan command khusus yang menghemat langkah berulang. Inilah topik episode kali ini — membuat Neovim bekerja untuk kalian, bukan sekadar dengan kalian.
Secara teknis, kita akan menguasai tiga mekanisme yang saling melengkapi:
vim.api.nvim_create_autocmd) — mengeksekusi kode saat suatu event terjadi di editor.vim.api.nvim_create_user_command) — membuat perintah baru bergaya :Nama yang bisa dipanggil dari command-line.Ketiganya adalah "reactive programming" dalam skala editor: sebuah sistem yang hidup, yang berubah perilakunya berdasarkan konteks. Sebagai DevOps/SRE, kalian pasti familiar dengan pola serupa — event-driven automation di Kubernetes (webhooks, controllers), CI/CD triggers, atau monitoring yang merespons metrics. Autocommand adalah cara Neovim mewujudkan pola yang sama.
Autocommand bekerja seperti webhook: Neovim memancarkan event tertentu selama siklus hidupnya, dan kalian mendaftarkan handler yang dijalankan saat event itu terjadi. Sintaks modernnya adalah vim.api.nvim_create_autocmd(events, opts).
vim.api.nvim_create_autocmd(events, {
pattern = "*", -- pola buffer yang dipantau (bisa "*.go")
callback = function() -- handler yang dijalankan
-- kode Lua di sini
end,
group = augroup, -- wadah untuk mencegah duplikasi (lihat bawah)
once = false, -- true = hanya dijalankan sekali
desc = "Keterangan",
})Event yang paling sering dipakai di config production:
| Event | Kapan Terjadi |
|---|---|
VimEnter | Setelah Neovim selesai startup |
BufRead / BufReadPost | Buffer selesai dibaca dari file |
BufNewFile | Buffer kosong untuk file baru dibuat |
BufWritePre | Tepat sebelum buffer ditulis ke disk |
BufWritePost | Tepat setelah buffer ditulis ke disk |
BufEnter | Kursor masuk ke sebuah buffer |
InsertLeave | Keluar dari insert mode |
TextYankPost | Setelah teks disalin (yank) |
FileType | Filetype sebuah buffer terdeteksi |
FocusGained / FocusLost | Window mendapat/kehilangan fokus |
CursorHold | Kursor diam selama beberapa saat |
lua/config/autocmds.luaBerikut file lengkap yang berisi tiga autocommand paling berguna untuk keseharian:
-- 1. Buat augroup unik untuk config kita.
-- clear = true menjamin handler tidak terdaftar dua kali.
local augroup = vim.api.nvim_create_augroup("UserConfig", { clear = true })
-- 2. Hapus trailing whitespace otomatis setiap kali menyimpan.
vim.api.nvim_create_autocmd("BufWritePre", {
group = augroup,
pattern = "*",
callback = function()
local save = vim.fn.winsaveview() -- simpan posisi kursor & folds
vim.cmd([[%s/\s\+$//e]]) -- hapus spasi di akhir baris
vim.fn.winrestview(save) -- pulihkan posisi kursor
end,
desc = "Hapus trailing whitespace saat menyimpan",
})
-- 3. Sorot sementara teks yang baru disalin (yank).
vim.api.nvim_create_autocmd("TextYankPost", {
group = augroup,
pattern = "*",
callback = function()
vim.highlight.on_yank({ timeout = 150 })
end,
desc = "Sorot teks saat yank",
})
-- 4. Kembali ke posisi kursor terakhir saat membuka file.
vim.api.nvim_create_autocmd("BufReadPost", {
group = augroup,
pattern = "*",
callback = function()
local last = vim.fn.line("'\"")
if last > 1 and last <= vim.fn.line("$") then
vim.api.nvim_win_set_cursor(0, { last, 0 })
end
end,
desc = "Kembali ke posisi terakhir saat membuka file",
})augroup Begitu Penting?Perhatikan baris pertama: vim.api.nvim_create_augroup("UserConfig", { clear = true }). Ini adalah pelindung dari bug paling klasik autocommand — handler yang didaftarkan berulang kali.
Warning
Setiap kali config dimuat ulang (misalnya dengan :luafile% atau saat Neovim restart dengan config yang memuat modul beberapa kali), autocommand baru akan didaftarkan di atas yang lama. Tanpa augroup, handler trailing-whitespace akan berjalan dua kali, tiga kali, bahkan belasan kali — sekali untuk setiap kali config dimuat. Di episode 9, plugin manager juga memuat ulang config secara rutin, jadi augroup adalah wajib, bukan opsional. clear = true membersihkan semua autocommand milik grup itu sebelum mendaftarkan ulang.
winsaveview() dan winrestview()?Ini detail yang sering diabaikan tapi menentukan kualitas hidup. Saat %s/\s\+$//e dijalankan di BufWritePre, posisi kursor dan state folds bisa bergeser. vim.fn.winsaveview() menyimpan state tampilan (posisi kursor, top line, folds), dan vim.fn.winrestview(save) memulihkannya setelah substitusi. Hasilnya: file dirapikan tanpa kalian menyadari apa pun terjadi — kursor tidak melompat, layar tidak berkedip.
patternAutocommand bisa dibatasi ke pola file tertentu. pattern = "*" berarti semua buffer; pattern = { "*.go", "*.py" } hanya berlaku untuk file Go dan Python. Ini penting untuk event seperti BufWritePre di mana kita tidak ingin merapikan file yang memang mengandung whitespace signifikan (misalnya file .md yang disengaja, atau file generasi otomatis).
:Autocommand bekerja otomatis; user command bekerja saat kalian panggil. Dengan vim.api.nvim_create_user_command, kalian bisa menambahkan perintah baru ke command-line Neovim yang berperilaku persis seperti perintah bawaan — lengkap dengan autocomplete.
vim.api.nvim_create_user_command(name, handler, opts)
-- name : nama perintah (wajib diawali huruf besar)
-- handler : fungsi Lua atau string perintah Ex
-- opts : nargs, range, complete, desc, bang, cmdtypeOpsi yang paling sering dipakai:
| Opsi | Nilai | Fungsi |
|---|---|---|
nargs | "0", "1", "*", "+", "?" | Jumlah argumen yang diizinkan |
range | "", "%", "<line1,line2>", 0/1/2 | Perintah menerima range baris |
complete | "file", "buffer", "help", dll. | Autocomplete argumen |
desc | string | Deskripsi perintah |
bang | true | Mengizinkan ! di akhir perintah |
cmdtype | "", "!", ">", "=" | Tipe command-line yang dipakai |
-- 1. Perintah sederhana: hapus trailing whitespace seluruh buffer
vim.api.nvim_create_user_command("TrimWhitespace", function()
vim.cmd([[%s/\s\+$//e]])
end, { desc = "Hapus trailing whitespace di seluruh buffer" })
-- 2. Perintah dengan argumen: sapa nama
vim.api.nvim_create_user_command("Greet", function(args)
print("Halo, " .. args.args .. "! Selamat datang di Neovim.")
end, { nargs = "*", desc = "Sapa nama", complete = "file" })
-- 3. Perintah dengan range: substitusi hanya pada baris yang dipilih
vim.api.nvim_create_user_command("ReplaceHello", function(args)
local start, finish = args.line1, args.line2
vim.api.nvim_buf_call(0, function()
vim.cmd(start .. "," .. finish .. "s/hello/halo/ge")
end)
end, { range = "%", desc = "Ganti hello dengan halo di range" })
-- 4. Perintah untuk menyalin path absolut ke clipboard
vim.api.nvim_create_user_command("CopyPath", function()
local path = vim.fn.expand("%:p")
vim.fn.setreg("+", path)
print("Path disalin: " .. path)
end, { desc = "Salin path absolut buffer ke clipboard" })Note
Objek args yang diterima handler berisi banyak informasi berguna: args.args adalah teks argumen yang diketik, args.line1 dan args.line2 adalah batas range yang diterapkan, args.fargs memecah argumen menjadi list (berguna untuk nargs = "*"), dan args.bang bernilai true jika perintah dipanggil dengan !. Inilah "API" yang membuat user command terasa sekuat perintah bawaan.
Important
Nama user command wajib diawali huruf besar (:TrimWhitespace), kecuali kalian mengaktifkan force = true dan memakai nama yang bukan perintah bawaan. Mengapa? Karena Neovim memakai huruf kecil untuk perintah bawaan, dan aturan ini mencegah kalian secara tidak sengaja menimpa perintah yang sudah ada. Jika kalian mencoba membuat :foo tanpa force, Neovim akan menolak dengan E183: User defined commands must start with an uppercase letter.
Kedua mekanisme ini bekerja paling baik saat digabung. Contoh nyata: format-on-save yang kita janjikan di episode 7. Alih-alih mengeraskan logika di autocmd, kita buat perintah :Format dulu:
vim.api.nvim_create_user_command("Format", function()
-- placeholder: di episode 17 kita ganti dengan conform.nvim
if vim.bo.filetype == "lua" then
vim.cmd("silent !stylua %")
end
end, { desc = "Format buffer dengan formatter sesuai bahasa" })
-- Panggil :Format otomatis sebelum menyimpan
vim.api.nvim_create_autocmd("BufWritePre", {
group = augroup,
pattern = { "*.lua" },
callback = function()
vim.cmd("Format")
end,
desc = "Format file Lua saat menyimpan",
})Pola command sebagai building block + autocmd sebagai pemicu ini sangat umum di config production-grade — kita akan melihatnya lagi di episode 17 saat membahas formatter sungguhan.
Sejauh ini semua autocommand kita global. Tapi salah satu kekuatan Neovim adalah kemampuannya menyesuaikan perilaku berdasarkan jenis file — inilah topik terakhir episode ini.
Neovim sudah mengaktifkan deteksi filetype secara default (filetype on). Saat membuka file, ia menentukan tipe file dari ekstensi, nama, atau isi file. Periksa hasilnya dengan :set filetype? — misalnya filetype=lua untuk init.lua, filetype=go untuk main.go.
Ada dua cara utama menyesuaikan perilaku per bahasa: ftplugin dan autocommand FileType. Yang paling idiomatik adalah ftplugin — file Lua yang ditempatkan di after/ftplugin/ dan otomatis dimuat setiap kali filetype tertentu terdeteksi.
~/.config/nvim/
└── after/
└── ftplugin/
├── lua.lua # dimuat untuk file .lua
├── go.lua # dimuat untuk file .go
├── python.lua # dimuat untuk file .py
├── typescript.lua
└── make.lua-- Opsi khusus Go: tab nyata (Go mewajibkan tab), indentasi 4 kolom
vim.opt_local.tabstop = 4
vim.opt_local.shiftwidth = 4
vim.opt_local.expandtab = false
vim.opt_local.smartindent = false-- Opsi khusus Python: indentasi 4 spasi, tampilkan nomor relatif
vim.opt_local.tabstop = 4
vim.opt_local.shiftwidth = 4
vim.opt_local.expandtab = true
vim.opt_local.relativenumber = trueTip
Perhatikan vim.opt_local — padanan Lua dari setlocal. Opsi yang di-set dengan opt_local hanya berlaku untuk buffer saat itu, sehingga pengaturan Go tidak akan bocor ke buffer Python. Inilah jawaban atas kesalahan umum "semua file jadi indentasi 2 spasi" — kesalahan yang terjadi justru saat memakai vim.opt (global) di ftplugin. Folder after/ftplugin/ dipakai karena dijamin dimuat setelah ftplugin bawaan, jadi pengaturan kita menang.
Alternatif ftplugin adalah autocommand FileType — cocok untuk logika yang tidak sekadar opsi:
vim.api.nvim_create_autocmd("FileType", {
group = augroup,
pattern = { "python", "lua", "javascript" },
callback = function()
vim.opt_local.expandtab = true
vim.opt_local.shiftwidth = 4
end,
desc = "Indentasi 4 spasi untuk bahasa tertentu",
})Kapan memakai yang mana? Gunakan ftplugin untuk set opsi sederhana (ringan, idiomatis, otomatis dimuat per buffer). Gunakan autocmd FileType saat butuh logika lebih kompleks atau sudah terintegrasi dengan augroup yang sama.
Kadang kalian membuka file orang lain yang punya konvensi berbeda — misalnya project yang memakai indentasi 2 spasi sementara default kalian 4. Modeline memungkinkan file menyatakan pengaturannya sendiri di dalam baris pertama atau terakhir file:
# vim: set ts=2 sw=2 expandtab:/* vim: set ts=2 sw=2 expandtab: */Saat Neovim membuka file dengan modeline seperti di atas, opsi ts, sw, dan expandtab otomatis di-set khusus untuk buffer tersebut — file "berbicara" kepada editor tentang cara dirinya harus ditampilkan. Ini sangat berguna untuk file dengan konvensi khusus seperti Makefile (wajib tab):
# vim: set noexpandtab ts=8 sw=8:Caution
Modeline adalah fitur yang bisa menjadi lubang keamanan. Sebuah file berbahaya dari internet bisa memuat modeline seperti vim: set guioptions=... :execute shell('...') yang mengeksekusi perintah di mesin kalian. Neovim secara default membatasi modeline pada opsi "safe" tertentu (:help 'modeline'), dan modelineexpr dinonaktifkan. Namun praktik terbaik tetap: untuk file yang tidak dipercaya, matikan modeline dengan :set nomodeline, atau aktifkan securemodelines (plugin). Jangan pernah membuka file mencurigakan dengan modeline aktif.
Note
Hierarki prioritas pengaturan, dari yang paling spesifik: modeline (dalam file) > ftplugin/FileType (per bahasa) > options.lua (global). Modeline menang karena ia paling dekat dengan konteks — file itu sendiri. Memahami hierarki ini membantu kalian menebak mengapa sebuah buffer tampil berbeda dari harapan.
augroup dengan { clear = true }.pattern = "*" padahal seharusnya spesifik. Autocommand BufWritePre dengan pattern = "*" akan merapikan trailing whitespace di semua buffer, termasuk file .log atau file yang sengaja punya spasi. Batasi dengan pola atau aktifkan secara buffer-local saat dibutuhkan.winsaveview() di BufWritePre sehingga kursor melompat ke baris 1 setiap kali menyimpan. Selalu simpan dan pulihkan view untuk autocommand yang memodifikasi buffer.desc. Perintah tetap bekerja, tapi sulit di-debug saat jumlahnya banyak. Biasakan menulis desc sejak awal.E183. Selalu mulai dengan huruf besar, atau gunakan force = true dengan sadar.vim.opt (global) di ftplugin sehingga pengaturan per-bahasa bocor ke buffer lain. Gunakan vim.opt_local.:set filetype?; jika kosong, file extension-nya belum dikenal. Kalian bisa menambahkan vim.filetype.add({ extension = { mdx = "markdown" } }).modeline saat membuka file mencurigakan.Tip
Debug autocommand dengan :autocmd untuk melihat semua handler terdaftar (dan dari grup mana), atau :autocmd BufWritePre untuk menyaring per event. Jika ada handler ganda, itu tanda augroup kalian bermasalah. Untuk user command, :command menampilkan daftar perintah, dan :help E183 menjelaskan aturan penamaan.
Di episode 8 ini kita telah mengubah Neovim dari editor pasif menjadi editor reaktif. Kalian menguasai autocommand dengan vim.api.nvim_create_autocmd — dari trim trailing whitespace saat BufWritePre, highlight teks yang di-yank, hingga memulihkan posisi kursor saat BufReadPost — semuanya terikat rapi dalam satu augroup agar tidak dobel. Kalian membangun user command kustom dengan vim.api.nvim_create_user_command, lengkap dengan nargs, range, dan complete. Terakhir, kalian memahami filetype detection, menyusun per-filetype options lewat after/ftplugin/, dan memanfaatkan modeline untuk file dengan konvensi khusus.
Poin penting yang harus kalian bawa:
augroup dengan { clear = true } untuk mencegah duplikasi.BufWritePre wajib memakai winsaveview() / winrestview().vim.opt_local di ftplugin agar pengaturan per-bahasa tidak bocor ke buffer lain.Kalian sekarang bisa membuat Neovim merespons event dan memahami konteks setiap file. Tapi ada satu lompatan besar yang belum kita lakukan: ekosistem plugin. Setiap plugin yang akan kita bahas dari episode 10 sampai akhir series — Treesitter, Telescope, LSP, formatter, sampai AI assistant — butuh sebuah fondasi untuk di-install dan dikelola dengan rapi. Di episode 9 selanjutnya, kita akan membangun fondasi itu: Plugin Manager Modern dengan lazy.nvim — bootstrap script, struktur lua/plugins/*.lua, dan UI :Lazy yang indah. Pastikan tetap semangat!