Di episode ini kita akan membangun engine autocompletion modern dengan nvim-cmp, menghubungkan sumber saran dari LSP, buffer, dan path, plus menguasai snippet engine LuaSnip dengan friendly-snippets siap pakai.

Setelah di episode 15 sebelumnya kita menghubungkan Neovim dengan language server lewat LSP — sekarang Neovim memahami kode kalian secara semantik, tapi ada satu hal yang membuat editor terasa hidup setiap kali kalian mengetik: popup autocompletion. Di episode ini kita akan membangun engine autocompletion modern dengan nvim-cmp dan menguasai snippet engine LuaSnip.
Pertimbangkan skenario nyata: kalian menulis handler baru di kode Go dan perlu memanggil fungsi GetUserByID yang ada di package lain. Tanpa completion, kalian harus ingat nama persisnya, mengetik manual, lalu berharap tidak salah eja. Dengan nvim-cmp, begitu kalian mengetik Get, muncul popup berisi saran dari basis data simbol LSP — lengkap dengan signature, dokumentasi, dan kemampuan Tab untuk melompat antar argumen. Rata-rata developer mengetik puluhan ribu karakter per hari; completion yang baik menghemat sebagian besar typo dan pencarian API, sehingga kecepatan berpikir kembali sejajar dengan kecepatan mengetik — filosofi yang kita bahas sejak episode 1.
nvim-cmp adalah completion engine yang popup-nya hanya satu dari banyak bagian. Kekuatan sesungguhnya ada pada arsitekturnya yang berbasis sources: setiap sumber saran adalah plugin terpisah, dan kalian bebas menggabungkannya.
| Source Plugin | Sumber Saran | Kapan Terasa Berguna |
|---|---|---|
cmp-nvim-lsp | Simbol dari language server (LSP) | Mengetik nama fungsi/variabel dari project & dependency |
cmp-buffer | Kata dari buffer yang sedang/kalau terbuka | Mengetik string atau istilah yang sama dengan kode sekitarnya |
cmp-path | Path file di sistem | Mengetik ./ atau ../ untuk import module |
cmp_luasnip | Snippet dari LuaSnip | Mengetik trigger snippet seperti fn atau fori |
cmp-cmdline | Command-line completion (:) | Mengetik perintah Ex seperti :bdelete |
Analogi yang tepat: nvim-cmp adalah kereta logistik, dan masing-masing source adalah pemasok barang. LSP pemasok utama (barang selalu segar dan akurat), buffer pemasok lokal (barang sisa yang kalian pernah pakai), path pemasok alamat, dan snippet pemasok "kotak paket jadi" yang tinggal diisi. Semua saran dijadikan satu antrian, di-ranking, lalu ditampilkan di popup yang sama.
Important
Kesalahan paling fatal bagi pemula: menginstall nvim-cmp tanpa cmp-nvim-lsp. Akibatnya popup completion tetap muncul, tapi tidak pernah berisi saran dari language server — persis seperti IDE yang kehilangan database simbolnya. Jika kalian sudah setting LSP di episode 15 tapi completion kosong, penyebab nomor satu adalah source ini tidak ada atau capabilities tidak di-passing.
Di episode 15 kita menyiapkan capabilities manual. Dengan nvim-cmp, ada helper yang lebih rapi:
-- Di dalam config nvim-lspconfig, GANTI blok capabilities ini:
local capabilities = vim.lsp.protocol.make_client_capabilities()
-- Ganti dengan:
local capabilities = require("cmp_nvim_lsp").default_capabilities()Fungsi default_capabilities() mengembalikan objek make_client_capabilities() yang sudah diisi semua kemampuan yang dibutuhkan nvim-cmp, termasuk completionItem.snippetSupport dan completionItem.resolveSupport. Ini menghilangkan kerumitan menyusun capabilities manual yang kita lakukan di episode 15 — satu baris, semua kebutuhan completion terpenuhi.
Tip
Karena capabilities didefinisikan sebelum handlers dijalankan dan dikirim ke semua server lewat handler, menukar blok ini sudah cukup untuk mengaktifkan completion LSP di semua bahasa yang terinstall — termasuk tsserver, pyright, dan gopls yang kita set di episode 15.
Ini jantung episode ini. Berikut spec lengkap yang production-grade:
return {
{
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"hrsh7th/cmp-cmdline",
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
"rafamadriz/friendly-snippets",
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
require("luasnip.loaders.from_vscode").lazy_load()
require("luasnip.loaders.from_lua").lazy_load({ paths = { vim.fn.stdpath("config") .. "/lua/snippets" } })
cmp.setup({
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif luasnip.expand_or_jumpable() then
luasnip.expand_or_jump()
else
fallback()
end
end, { "i", "s" }),
["<S-Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif luasnip.jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end, { "i", "s" }),
["<CR>"] = cmp.mapping.confirm({ select = true }),
["<C-Space>"] = cmp.mapping.complete(),
["<C-e>"] = cmp.mapping.abort(),
["<C-n>"] = cmp.mapping.select_next_item(),
["<C-p>"] = cmp.mapping.select_prev_item(),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-b>"] = cmp.mapping.scroll_docs(-4),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "buffer" },
{ name = "path" },
}),
})
end,
},
}Mari kita bedah blok pentingnya:
snippet.expand — critical piece yang menyatukan nvim-cmp dengan LuaSnip. Ketika sebuah snippet terpilih, fungsi ini memanggil luasnip.lsp_expand untuk mengeksekusinya. Jika fungsi ini hilang atau salah engine, snippet tidak akan pernah bisa di-expand — persis masalah klasik "snippet muncul di popup tapi tidak bekerja".mapping dengan cmp.mapping.preset.insert(...) — preset yang menyediakan pemetaan default yang sehat, lalu kita tambahkan yang kustom.<Tab> — prioritas berantai yang elegan: jika popup visible, Tab memilih item berikutnya; jika tidak dan ada snippet yang bisa di-jump, Tab melompat ke placeholder snippet berikutnya; jika tidak keduanya, jatuh ke fungsi fallback (Tab bawaan Vim).sources — urutan menentukan prioritas default: LSP selalu paling atas, lalu snippet, buffer, dan path sebagai pelengkap.Caution
Konflik Tab adalah musuh terbesar nvim-cmp. Jika kalian punya plugin lain yang memakai <Tab> (misal tmux navigasi, auto-pair, atau which-key), mereka akan berebut keybinding. Solusi: urutkan logika di dalam satu callback Tab seperti contoh di atas, atau pindahkan plugin lain ke kombinasi tombol berbeda. Jangan mendaftarkan dua keymap <Tab> terpisah untuk fitur berbeda — itu resep konflik yang membingungkan.
Ketika banyak source mengirim saran sekaligus, pertanyaan berikutnya adalah: saran mana yang tampil paling atas? nvim-cmp memakai comparators yang dievaluasi berurutan seperti penyaring pada pipeline — saran yang menang di comparator pertama langsung dianggap lebih baik tanpa perlu mengecek sisanya:
sorting = {
priority_weight = 2.0,
comparators = {
cmp.config.compare.offset,
cmp.config.compare.exact,
cmp.config.compare.score,
cmp.config.compare.recently_used,
cmp.config.compare.locality,
cmp.config.compare.kind,
cmp.config.compare.sort_text,
cmp.config.compare.length,
cmp.config.compare.order,
},
},Alur kerjanya:
offset — saran yang persis mengikuti kursor menang (posisi completion lebih relevan).exact — kecocokan persis dengan teks yang diketik menang atas kecocokan parsial.score — skor internal fuzzy matching dari masing-masing source.recently_used — saran yang pernah dipilih sebelumnya dinaikkan (pembelajaran dari kebiasaan).locality — saran yang lebih dekat dengan posisi kursor dalam file diunggulkan.length — saran lebih pendek menang untuk kasus yang hampir seri.Tip
Jika kalian merasa popup terlalu sering menampilkan saran yang tidak relevan, paling sering penyebabnya adalah comparator locality dan length yang terlalu cepat dipakai. Tweak paling umum: hapus locality untuk source LSP (biarkan ia menang murni berdasarkan score), atau naikkan priority_weight untuk menekankan source tertentu. Nilai default sudah cukup untuk mayoritas developer — ubah hanya jika terasa mengganggu.
Sekarang mari kita lihat bagaimana semua bagian bekerja sama dalam satu sesi menulis kode:
1. Ketik "function getUser" → popup menampilkan: getUserByID (LSP),
getUserByEmail (LSP), getUserFromCache (buffer), dst.
2. Tekan <C-n> / <C-p> untuk berpindah saran tanpa melepas Ctrl.
3. Tekan <C-b> untuk scroll dokumentasi saran yang sedang disorot.
4. Tekan <CR> untuk memilih → kode terisi + signature helper muncul.
5. Ketik trigger "fori" → pilih snippet → expand → Tab melalui placeholder.
6. Tekan <C-Space> kapan saja untuk memaksa popup muncul kembali.Perhatikan bahwa seluruh interaksi dilakukan di home row, tanpa menyentuh mouse maupun angka — konsisten dengan filosofi modal editing yang kita bangun sejak episode 2.
Satu fitur yang sering luput dari perhatian: nvim-cmp juga bisa melengkapi command-line (:). Dengan cmp-cmdline, mengetik :bde akan menampilkan :bdelete, dan :set nos akan menampilkan semua opsi nos.... Ini mempercepat penggunaan perintah Ex yang sering diketik berulang:
-- Setup command-line completion terpisah dari buffer completion
cmp.setup.cmdline(":", {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({
{ name = "path" },
}, {
{ name = "cmdline" },
}),
})Karena command-line punya konteks berbeda (perintah vs path), source-nya diisolasi dari completion normal — kalian mendapat saran perintah ketika mengetik perintah, dan saran path ketika argumen perintah adalah file.
LuaSnip adalah engine snippet yang cepat, ditulis dalam Lua, dan terintegrasi langsung dengan nvim-cmp. Ia mendukung tiga gaya snippet sekaligus:
from_vscode) — format standar yang bisa diimpor dari repositori snippet komunitas seperti friendly-snippets, yang berisi puluhan ribu snippet siap pakai untuk semua bahasa.from_lua) — snippet didefinisikan langsung dengan tabel Lua, paling ekspresif.from_snipmate) — format lama Vim.Dua baris pemuat di config kita sudah mencakup yang paling penting: from_vscode memuat semua snippet friendly-snippets, dan from_lua memuat snippet kustom yang kita tulis sendiri.
Setelah snippet ter-expand, kalian akan melihat placeholder yang bisa diisi. Workflow-nya:
fori lalu tekan <Tab> — nvim-cmp menampilkan snippet "for index" dari friendly-snippets.<CR> untuk memilih — snippet langsung ter-expand menjadi blok for lengkap dengan kursor di placeholder i = 1.<Tab> — kursor melompat ke placeholder berikutnya (kondisi loop).<S-Tab> untuk kembali ke placeholder sebelumnya.Ini adalah tabstop navigation — persis seperti feature yang membuat VS Code snippet begitu nyaman. Bedanya, di Neovim semua ini berjalan tanpa mouse dan tanpa meninggalkan keyboard home row.
friendly-snippets luar biasa, tapi tim production hampir selalu butuh snippet spesifik — misalnya pola service atau handler khusus yang sering ditulis. Mari kita buat file snippet Lua sendiri:
return {
s("fn", {
t({ "function ", "" }),
i(1, "name"),
t({ "(", "" }),
i(2, "param"),
t({ ")", "\t", "" }),
i(3, "body"),
t({ "", "end", "" }),
}),
s("fori", {
t({ "for ", "" }),
i(1, "i"),
t({ " = ", "" }),
i(2, "1"),
t({ ", ", "" }),
i(3, "n"),
t({ " do", "\t", "" }),
i(4, "body"),
t({ "", "end", "" }),
}),
s("req", t('local M = require("$1")\nreturn M')),
}Struktur node LuaSnip:
s(trigger, nodes) — membuat snippet baru dengan trigger string.t("teks") — teks statis (bisa berisi \t untuk tab, \n untuk baris baru).i(n, "default") — insert node (placeholder) bernomor n. Nomor menentukan urutan navigasi Tab.c(1, { ... }) — choice node (pilih di antara beberapa opsi) — fitur yang tidak dimiliki format VS Code sederhana.Dengan satu file kecil ini, mengetik fn lalu <Tab> menghasilkan template fungsi lengkap dalam tiga ketukan. Kalikan dengan pola-pola yang sering kalian tulis di tim, dan hitung berapa banyak waktu yang dihemat setiap hari.
| Kesalahan | Gejala | Solusi |
|---|---|---|
| Popup kosong dari LSP | Completion tidak pernah berisi saran dari project | Tambahkan cmp-nvim-lsp ke dependencies & sources; pastikan capabilities memakai cmp_nvim_lsp.default_capabilities() |
| Snippet tidak bisa expand | Memilih snippet tapi tidak terjadi apa-apa | Pastikan snippet.expand memakai luasnip.lsp_expand; jangan memakai engine lain |
| Tab tidak berfungsi | Tab malah memberi indentasi atau tidak melakukan apa-apa | Cek konflik keymap Tab; pastikan { "i", "s" } sebagai mode |
<CR> memilih item saat menulis kode normal | Enter yang tidak diinginkan | Ubah <CR> mapping menjadi cmp.mapping.confirm({ select = false }) atau hapus mapping |
| Snippet terpilih dua kali / duplikat | Item snippet muncul ganda di popup | Hapus cmp_luasnip atau LuaSnip jika terduplikat di sources |
| Snippet custom tidak muncul | File snippet Lua tidak ter-load | Pastikan path di from_lua benar dan file return { ... } punya tabel snippet |
| Performance menurun di file besar | Popup terasa berat saat mengetik | Matikan cmp-buffer untuk buffer raksasa atau set max_item_count |
| Saran dari LSP hilang setelah update | Ternyata capabilities ditimpa di tempat lain | Cek handler lspconfig — capabilities harus dipassing ke setup() setiap server |
Warning
Perangkap tersembunyi: friendly-snippets memuat snippet untuk semua bahasa secara default. Di project dengan banyak bahasa, ini menambah beban minor dan bisa memunculkan snippet yang tidak relevan di filetype tertentu. Solusi bersih: ganti require("luasnip.loaders.from_vscode").lazy_load() dengan pemuatan selektif per bahasa (lazy_load({ paths = { "~/some/snippets" } })), atau atur filetype di sources supaya luasnip hanya aktif untuk bahasa yang memang butuh.
Pada episode 16 ini kita telah membangun engine autocompletion yang lengkap: nvim-cmp sebagai pusatnya, empat source utama (LSP, buffer, path, snippet) yang diatur prioritasnya, cmp_nvim_lsp.default_capabilities() untuk menghubungkan ke LSP episode 15, serta LuaSnip dengan friendly-snippets untuk expand-and-jump workflow yang mulus. Kalian juga sekarang bisa menulis snippet kustom — senjata rahasia untuk mempercepat pola-pola berulang di tim kalian.
Sekarang saat kalian mengetik, popup saran muncul, snippet ter-expand dengan tabstop navigation, dan Neovim terasa seperti IDE modern sesungguhnya. Namun ada satu sisi yang belum kita sentuh: konsistensi kode. Saran yang benar itu penting, tapi kode yang ditulis dalam format yang seragam dengan tim adalah standar profesional yang tak bisa ditawar.
Di episode 17 selanjutnya, kita akan membahas Code Formatting & Linting dengan conform.nvim dan nvim-lint — memformat kode secara otomatis saat menyimpan file dan memeriksa kualitas kode secara asinkron tanpa membekukan editor. Pastikan tetap semangat!