Di episode ini kita akan mengotomatiskan pemformatan kode saat menyimpan file dengan conform.nvim dan menjalankan static analysis asinkron dengan nvim-lint, ditutup dengan navigasi diagnostics yang efisien.

Setelah di episode 16 sebelumnya kita membangun autocompletion yang responsif dengan nvim-cmp dan LuaSnip, pada episode kali ini kita akan menyelesaikan puzzle kualitas kode: formatting otomatis dan linting asinkron dengan conform.nvim dan nvim-lint.
Di dunia nyata, format-on-save dan linting adalah bagian dari culture code tim. Bayangkan kalian bergabung dengan codebase besar dengan 10 contributor: tanpa formatter, setiap orang menulis indentasi 2 spasi atau 4 spasi, kutip tunggal atau ganda, dan diff pull request dipenuhi perubahan yang tidak relevan. Tim yang matang memakai formatter untuk menghapus argumentasi style dari diskusi code review — debat difokuskan pada logika, bukan soal spasi. Sementara itu, linter bekerja sebagai silent reviewer: menangkap variabel yang tidak terpakai, kondisi yang tidak pernah benar, atau pola berbahaya sebelum kode sampai ke CI. Menjadikan keduanya berjalan otomatis di editor adalah standar engineering modern — dan itu semua bisa hidup di dalam Neovim.
Sebelum masuk ke konfigurasi, penting membedakan dua tanggung jawab yang sering tertukar:
| Aspek | Formatting | Linting |
|---|---|---|
| Tujuan | Normalisasi gaya: indentasi, spasi, kutip, panjang baris | Deteksi bug & anti-pattern: unused var, error-prone code |
| Hasil | Kode ditulis ulang (mutasi file) | Hanya melaporkan masalah (diagnostic) |
| Contoh | Prettier, Stylua, Gofmt | ESLint, ShellCheck, Flake8 |
| Waktu terbaik | Saat menyimpan file (BufWritePre) | Terus-menerus, tanpa memblokir pengetikan |
Formatting mengubah kode, linting hanya menandai. Keduanya saling melengkapi: kode bisa lolos format tapi tetap bermasalah, dan sebaliknya.
Fitur yang terlihat sepele ini menyelesaikan masalah konsistensi yang sebenarnya rumit secara sosial: kapan format dilakukan? Jika formatter hanya berjalan saat command tertentu, manusia akan lupa — dan diff tetap berantakan. Dengan format-on-save, aturannya deterministik: setiap file yang disimpan akan diformat. Tidak ada lagi "oh, PR ini menambah 40 baris perubahan format". Ini adalah automation over negotiation — kebijakan dijalankan mesin, bukan diharapkan dari disiplin manusia.
conform.nvim oleh stevearc adalah formatter runner modern: ia menjalankan binary formatter eksternal (Prettier, Stylua, dan lainnya) secara asinkron dan menulis hasilnya ke buffer. Berikut spec lengkapnya:
return {
{
"stevearc/conform.nvim",
event = { "BufWritePre" },
cmd = { "ConformInfo" },
keys = {
{
"<leader>f",
function()
require("conform").format({ async = true })
end,
desc = "Format file",
},
},
opts = {
formatters_by_ft = {
lua = { "stylua" },
javascript = { "prettier" },
typescript = { "prettier" },
javascriptreact = { "prettier" },
typescriptreact = { "prettier" },
json = { "prettier" },
html = { "prettier" },
css = { "prettier" },
scss = { "prettier" },
python = { "ruff_format", "ruff_organize_imports" },
go = { "goimports", "gofmt" },
sh = { "shfmt" },
yaml = { "yamlfmt" },
markdown = { "prettier" },
},
format_on_save = {
timeout_ms = 2000,
lsp_format = "fallback",
},
},
},
}Bedah poin penting:
formatters_by_ft — peta filetype → daftar formatter. Daftar dieksekusi berurutan dari kiri ke kanan: untuk Python, ruff_format menjalankan format dulu, lalu ruff_organize_imports merapikan import. Untuk Go, goimports menata import terlebih dahulu, kemudian gofmt menormalkan format.format_on_save — memicu format setiap BufWritePre. timeout_ms = 2000 memberi waktu maksimal 2 detik sebelum memberi tahu pengguna, dan lsp_format = "fallback" berarti: jika tidak ada formatter yang cocok untuk filetype, coba gunakan formatting dari language server (LSP). Ini safety net yang membuat file jenis baru tetap terformat tanpa konfigurasi tambahan.event = { "BufWritePre" } — plugin hanya dimuat ketika kalian menyimpan file, karena format-on-save adalah satu-satunya alasan plugin ini dibutuhkan. Startup time tetap minim.Note
Perhatikan bahwa formatter dijalankan sebagai proses eksternal. conform.nvim tidak menulis kode format sendiri — ia memanggil binary seperti prettier atau stylua yang harus sudah ada di sistem. Di sinilah Mason (episode 15) berperan: :MasonInstall prettier stylua shfmt yamlfmt ruff meng-install semuanya dalam satu marketplace. Pastikan formatter terinstall sebelum mengharapkan format bekerja.
| Bahasa | Formatter | Sumber Instalasi | Catatan |
|---|---|---|---|
| JS / TS / JSON / HTML / CSS / Markdown | prettier | Mason / npm | Satu tool untuk banyak format; konfigurasi via .prettierrc |
| Lua | stylua | Mason | Standar de-facto untuk Lua |
| Python | ruff_format + ruff_organize_imports | Mason (ruff) | Jauh lebih cepat dari Black; juga bisa jadi linter |
| Python (alternatif) | black | Mason / pip | Populer, tapi lambat dibanding ruff |
| Go | gofmt + goimports | Go toolchain | gofmt bawaan Go; goimports menata import |
| Shell / Bash | shfmt | Mason | Normalisasi indentasi shell |
| YAML | yamlfmt | Mason | Alternatif yang lebih cepat dari prettier untuk YAML |
Note
Perhatikan perbedaan dua kategori besar formatter. Formatter "universal" seperti Prettier dikonfigurasi lewat file di root project (.prettierrc, package.json) — ia mengikuti standar project, bukan standar editor. Sedangkan formatter per-bahasa seperti Stylua dan Gofmt bersifat deterministik (Gofmt bahkan terkenal tanpa opsi konfigurasi sama sekali — itulah filosofi "satu cara format untuk Go"). Ketika kalian berpindah antar project, formatter universal yang membuat editor kalian menyesuaikan diri, bukan sebaliknya.
Format-on-save sangat ideal, tapi ada situasi yang menuntut kendali manual:
conform.nvim menangani semua ini tanpa meninggalkan editor:
:ConformInfo " cek formatter mana yang aktif untuk buffer ini
:Conform format " format buffer aktif (tanpa menyimpan)
:'<,'>Conform format " format hanya range visual yang dipilihKombinasi dengan keymap <leader>f yang kita definisikan di spec membuat kalian punya dua mode: otomatis saat save untuk rutinitas, dan manual saat membutuhkan presisi. Kebijakan yang fleksibel ini biasanya lebih disukai tim daripada memaksa format-on-save tanpa kecuali.
Tip
Untuk tim yang menerapkan "don't format other people's code", pola yang banyak dipakai: nonaktifkan format-on-save secara global, aktifkan hanya untuk filetype tertentu via autocmd, atau biarkan manual <leader>f sebagai satu-satunya pemicu. Diskusikan kebijakan ini dengan tim dan tuangkan dalam konfigurasi bersama (dotfiles) agar semua developer konsisten.
Jika formatter menulis ulang kode, linter membaca dan melaporkan. nvim-lint adalah runner linter asinkron yang ringan: ia tidak membuat UI sendiri, melainkan menyalurkan hasil linter ke sistem diagnostics bawaan Neovim — jendela yang sama yang dipakai LSP di episode 15.
return {
{
"mfussenegger/nvim-lint",
event = { "BufReadPost", "BufWritePost" },
config = function()
local lint = require("lint")
lint.linters_by_ft = {
javascript = { "eslint_d" },
typescript = { "eslint_d" },
javascriptreact = { "eslint_d" },
typescriptreact = { "eslint_d" },
sh = { "shellcheck" },
python = { "ruff" },
lua = { "luacheck" },
}
vim.api.nvim_create_autocmd({ "BufWritePost", "BufEnter" }, {
callback = function()
lint.try_lint()
end,
})
end,
},
}Bedah logikanya:
linters_by_ft — peta filetype → linter. eslint_d adalah versi daemon dari ESLint yang jauh lebih cepat untuk re-run berulang; shellcheck untuk shell; ruff untuk Python (dua peran: formatter dan linter); luacheck untuk Lua.autocmd dengan try_lint() — menjalankan linting saat file disimpan (BufWritePost) dan saat buffer dimasuki (BufEnter). try_lint() adalah fungsi idempotent: jika linter tidak tersedia atau sudah berjalan, ia tidak akan menduplikasi kerja.Important
Kekuatan utama nvim-lint adalah asinkron. Linter dijalankan di luar event loop Neovim, sehingga mengetik tidak pernah tersendat — hasilnya muncul sebagai garis-garis warna di gutter (yang kita siapkan signcolumn di episode 7) tanpa membekukan editor. Ini kontras dengan menjalankan linter secara sinkron di terminal, yang memaksa kalian menunggu proses selesai.
Sekarang ada dua sumber diagnostic: LSP (episode 15) dan nvim-lint — keduanya masuk ke sistem yang sama. Karena itu, semua keymap navigasi diagnosis memakai API vim.diagnostic yang universal:
local map = vim.keymap.set
map("n", "[d", vim.diagnostic.goto_prev, { desc = "Diagnostic sebelumnya" })
map("n", "]d", vim.diagnostic.goto_next, { desc = "Diagnostic berikutnya" })
map("n", "<leader>e", vim.diagnostic.open_float, { desc = "Detail diagnostic" })
map("n", "<leader>q", vim.diagnostic.setloclist, { desc = "Semua diagnostic ke list" })Alur kerja diagnostic yang produktif:
]d untuk melompat ke error berikutnya — kursor langsung pindah, tidak perlu scroll manual.<leader>e untuk membuka popup detail: pesan error, kode, dan hint.<leader>q untuk mengirim semuanya ke location list dan melihatnya sebagai daftar terfilter di bawah.Perbedaan dua "daftar" di Vim/Neovim yang sering membingungkan:
| Konsep | Perintah | Cakupan | Kapan Dipakai |
|---|---|---|---|
| Quickfix list | :copen, :cnext, :cprev | Seluruh project | Hasil grep (:grep), build errors, kompilasi |
| Location list | :lopen, :lnext, :lprev | Buffer/window aktif | Diagnostics buffer (kasus kita), results per-window |
Tip
Karena setloclist mengisi location list (scoped per-window), daftar diagnostics tidak tercampur dengan quickfix yang dipakai proses lain seperti grep. Jika kalian ingin semuanya masuk ke quickfix global, ganti dengan vim.diagnostic.setqflist — pilihan keduanya valid, tinggal sesuaikan dengan alur kerja tim kalian.
| Kesalahan | Gejala | Solusi |
|---|---|---|
| Formatter tidak terinstall | Error conform: no formatter configured for ... atau proses gagal | :MasonInstall prettier stylua shfmt ruff yamlfmt; cek :ConformInfo |
| Prettier tidak menemukan konfigurasi | Format menghasilkan gaya tak terduga | Sediakan .prettierrc di root project; Prettier membaca konfigurasi dari project (resolusi bottom-up) |
| Format ganda / bentrok dengan LSP | Kode diformat dua kali dengan hasil berbeda | Atur lsp_format = "never" jika formatter konform sudah pasti; biarkan fallback hanya saat tidak ada formatter |
| Linter belum terinstall | Diagnostic tidak pernah muncul | Install via Mason (shellcheck, ruff, luacheck, eslint_d); cek :LintInfo |
eslint vs eslint_d kebingungan | Linter tidak jalan di project dengan ESLint version sendiri | Pastikan eslint_d diinstall dan project punya konfigurasi ESLint |
| Quickfix vs location list tertukar | Daftar hasil tidak ada di tempat yang diharapkan | Pahami scope keduanya; setqflist untuk project-wide, setloclist untuk buffer |
| Format-on-save memakan waktu lama | Menyimpan file terasa lambat | Naikkan timeout_ms atau jadikan formatter manual (<leader>f) untuk file raksasa |
| Prettier mencegah save karena error config | File tidak tersimpan | Perbaiki .prettierrc; atau gunakan prettierd sebagai fallback yang lebih toleran |
| Diff PR penuh perubahan format | Reviewer keberatan dengan diff yang tidak relevan | Pastikan format-on-save aktif sebelum menulis kode; jangan format ulang file lama tanpa koordinasi tim |
Warning
Satu jebakan yang sering membuat developer jengkel: Prettier dan ESLint kadang merekomendasikan gaya yang bertentangan (misal soal semicolon atau trailing comma). Jika keduanya diaktifkan sekaligus, diff bisa berubah-ubah. Solusi standar tim modern: biarkan Prettier (formatter) yang menangani gaya, dan matikan semua aturan format di ESLint (eslint-plugin-prettier atau meng-set aturan format ke off). Prinsipnya: satu tool untuk satu tanggung jawab.
Pada episode 17 ini kita telah membangun dua pilar kualitas kode: conform.nvim untuk format-on-save yang deterministik dengan peta formatter per bahasa (Prettier, Stylua, Ruff, Gofmt, shfmt), dan nvim-lint untuk static analysis asinkron (ESLint, ShellCheck, Ruff, Luacheck) yang menyatu dengan sistem diagnostics Neovim. Kita juga menguasai navigasi diagnostics ([d, ]d, <leader>e, <leader>q) serta perbedaan quickfix vs location list.
Kode kalian kini tidak hanya lengkap dan benar secara semantik, tapi juga konsisten secara gaya dan bersih secara kualitas — tiga standar sekaligus yang biasanya membutuhkan tiga tools terpisah di IDE komersial.
Masih ada satu dimensi yang belum kita sentuh dalam perjalanan ini: kolaborasi. Semua pekerjaan di editor kita hingga saat ini bersifat individual. Di dunia nyata, kode adalah produk tim yang hidup dalam version control.
Di episode 18 selanjutnya, kita akan membahas Integrasi Git di Dalam Neovim dengan gitsigns.nvim dan diffview.nvim — melihat perubahan kode langsung di gutter, blame inline, stage/reset hunk, hingga resolusi merge conflict tanpa meninggalkan editor. Pastikan tetap semangat!