Belajar Neovim - Autocommands, Filetype Detection & Custom Commands
Episode 8 of 28

Belajar Neovim - Autocommands, Filetype Detection & Custom Commands

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.

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

Pendahuluan

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:

  1. Autocommands (vim.api.nvim_create_autocmd) — mengeksekusi kode saat suatu event terjadi di editor.
  2. Custom User Commands (vim.api.nvim_create_user_command) — membuat perintah baru bergaya :Nama yang bisa dipanggil dari command-line.
  3. Filetype Detection & Modeline — membuat Neovim menyesuaikan perilaku secara otomatis berdasarkan jenis file yang sedang dibuka.

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: Merespons Event di Editor

Konsep Dasar

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

Sintaks nvim_create_autocmd
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:

EventKapan Terjadi
VimEnterSetelah Neovim selesai startup
BufRead / BufReadPostBuffer selesai dibaca dari file
BufNewFileBuffer kosong untuk file baru dibuat
BufWritePreTepat sebelum buffer ditulis ke disk
BufWritePostTepat setelah buffer ditulis ke disk
BufEnterKursor masuk ke sebuah buffer
InsertLeaveKeluar dari insert mode
TextYankPostSetelah teks disalin (yank)
FileTypeFiletype sebuah buffer terdeteksi
FocusGained / FocusLostWindow mendapat/kehilangan fokus
CursorHoldKursor diam selama beberapa saat

Membangun lua/config/autocmds.lua

Berikut file lengkap yang berisi tiga autocommand paling berguna untuk keseharian:

lua/config/autocmds.lua
-- 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",
})
Autocommand inti — semua terikat dalam satu augroup

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

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

Membatasi dengan pattern

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

Custom User Command: Memperluas Bahasa :

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.

Sintaks Dasar

Sintaks nvim_create_user_command
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, cmdtype

Opsi yang paling sering dipakai:

OpsiNilaiFungsi
nargs"0", "1", "*", "+", "?"Jumlah argumen yang diizinkan
range"", "%", "<line1,line2>", 0/1/2Perintah menerima range baris
complete"file", "buffer", "help", dll.Autocomplete argumen
descstringDeskripsi perintah
bangtrueMengizinkan ! di akhir perintah
cmdtype"", "!", ">", "="Tipe command-line yang dipakai

Contoh Praktis

Membuat user command kustom
-- 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.

Aturan Penamaan: Huruf Besar Pertama

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.

Autocommand + User Command: Satu Kesatuan

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:

Format on save dengan user command
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",
})
Command :Format dipanggil otomatis oleh autocmd

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.

Filetype Detection & Modeline

Sejauh ini semua autocommand kita global. Tapi salah satu kekuatan Neovim adalah kemampuannya menyesuaikan perilaku berdasarkan jenis file — inilah topik terakhir episode ini.

Bagaimana Neovim Mendeteksi Filetype

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.

Per-filetype Options dengan ftplugin

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.

Struktur after/ftplugin/
~/.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
~/.config/nvim/after/ftplugin/go.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
~/.config/nvim/after/ftplugin/python.lua
-- 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 = true

Tip

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:

Autocmd FileType untuk logika
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.

Modeline: Pengaturan dalam File Itu Sendiri

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:

plaintext
# vim: set ts=2 sw=2 expandtab:
javascript
/* 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):

plaintext
# 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.

Kesalahan Umum Autocommand, Filetype & Custom Commands

  1. Handler autocommand terdaftar dua kali karena config dimuat ulang tanpa augroup. Solusi: selalu buat augroup dengan { clear = true }.
  2. Memakai 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.
  3. Lupa winsaveview() di BufWritePre sehingga kursor melompat ke baris 1 setiap kali menyimpan. Selalu simpan dan pulihkan view untuk autocommand yang memodifikasi buffer.
  4. User command tanpa desc. Perintah tetap bekerja, tapi sulit di-debug saat jumlahnya banyak. Biasakan menulis desc sejak awal.
  5. Nama user command berhuruf kecil → error E183. Selalu mulai dengan huruf besar, atau gunakan force = true dengan sadar.
  6. Memakai vim.opt (global) di ftplugin sehingga pengaturan per-bahasa bocor ke buffer lain. Gunakan vim.opt_local.
  7. Filetype yang tidak terdeteksi. Cek :set filetype?; jika kosong, file extension-nya belum dikenal. Kalian bisa menambahkan vim.filetype.add({ extension = { mdx = "markdown" } }).
  8. Modeline diaktifkan untuk file yang tidak dipercaya — risiko keamanan. Matikan 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.

Penutup

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:

  • Selalu daftarkan autocommand dalam augroup dengan { clear = true } untuk mencegah duplikasi.
  • Autocommand yang memodifikasi buffer saat BufWritePre wajib memakai winsaveview() / winrestview().
  • Nama user command harus diawali huruf besar.
  • Gunakan vim.opt_local di ftplugin agar pengaturan per-bahasa tidak bocor ke buffer lain.
  • Modeline adalah fitur kuat namun rawan keamanan — waspadai file yang tidak dipercaya.

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!

Belajar Neovim - Autocommands, Filetype Detection & Custom Commands | Belajar Neovim