Quản lý plugin với lazy.nvim và hướng mở rộng với vim.pack
Bài trước bạn đã có init.lua gọn gàng và một thư mục lua/plugins/ để chứa plugin. Giờ chúng ta cắm "động cơ" vào: một plugin manager biết cài, biết load đúng lúc. Nguyên tắc tôi luôn dặn người mới: mỗi dòng config phải trả lời được câu hỏi "dòng này để làm gì". Nếu không trả lời được, xoá đi.
Bạn sẽ làm được gì sau bài này
- Viết đoạn bootstrap để Neovim tự cài lazy.nvim, kể cả khi
nvim-datacòn trống trên Windows 11. - Đọc và viết được một plugin spec với
event,cmd,keys,ft,dependencies,opts,lazy. - Biết khi nào nên giữ lazy.nvim và khi nào chỉ cần
vim.packcó sẵn trong Neovim.
Dựng lazy.nvim từ con số 0
Đây là đoạn duy nhất trong init.lua được phép "cứng", vì nó phải chạy trước khi có bất kỳ plugin nào:
-- ~/AppData/Local/nvim/init.lua
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none", "--branch=stable",
"https://github.com/folke/lazy.nvim.git", lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup("plugins", {
change_detection = { notify = false },
})
Giải thích từng phần, vì đây là chỗ nhiều người copy mà không hiểu:
vim.fn.stdpath("data")trả về thư mục dữ liệu, trên Windows 11 làC:\Users\<tên>\AppData\Local\nvim-data. Plugin không nằm trong thư mục config, nên xoá config không mất plugin.(vim.uv or vim.loop)là cách viết tương thích: Neovim 0.10 đổi tênvim.loopthànhvim.uv. Dùng cách này thì config chạy được ở cả hai bản.vim.fn.system({...})gọigit clonevới danh sách tham số, không nối chuỗi, nên đường dẫn có dấu cách vẫn an toàn.vim.opt.rtp:prepend(lazypath)là bước hay bị quên: phải đưa lazy.nvim vàoruntimepaththìrequire("lazy")mới tìm thấy.require("lazy").setup("plugins")nghĩa là import toàn bộ file tronglua/plugins/, mỗi filereturnmột spec (hoặc một mảng spec).
Sign in to view the full content of this lesson. Sign in
Warning: Bootstrap cần
gitcó trongPATH. Trên Windows 11, nếu bạn cài Git nhưng chưa thêm vàoPATH, đoạn clone sẽ thất bại và Neovim không báo lỗi rõ ràng. Kiểm tra bằng:!git --versionngay sau khi mở Neovim.
Sau lần chạy đầu, plugin được clone vào nvim-data\lazy\lazy.nvim, và mọi plugin khác nằm cạnh đó trong nvim-data\lazy\.
Giải phẫu một plugin spec: lazy loading nằm ở đâu
Spec chỉ là một bảng Lua. Các field quyết định "khi nào load", không phải "load cái gì":
| Field | Ý nghĩa | Dùng khi |
|---|---|---|
event |
Load khi có event (BufReadPre, VeryLazy, ...) |
Plugin cần hiện diện khi mở file |
cmd |
Load khi gõ command | Plugin expose một :Command |
keys |
Load khi bấm keybinding | Plugin chỉ dùng qua phím tắt |
ft |
Load theo filetype | Plugin riêng cho ngôn ngữ |
dependencies |
Plugin phụ, load kèm | Plugin cần thư viện nền |
opts |
Bảng truyền cho setup() |
Trường hợp phổ biến nhất |
config |
Hàm tự chạy khi load | Khi cần logic ngoài setup() |
lazy = false |
Load ngay khi khởi động | Chỉ dùng cho plugin nền tảng |
Ví dụ thật, dùng cho chính config của bạn:
-- lua/plugins/gitsigns.lua
return {
"lewis6991/gitsigns.nvim",
event = { "BufReadPre", "BufNewFile" },
opts = { signs = { add = { text = "│" } } },
}
-- lua/plugins/telescope.lua
return {
"nvim-telescope/telescope.nvim",
cmd = "Telescope",
keys = {
{ "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Tìm file" },
{ "<leader>fg", "<cmd>Telescope live_grep<cr>", desc = "Grep trong dự án" },
},
dependencies = { "nvim-lua/plenary.nvim" },
opts = {},
}
Điểm mấu chốt: một plugin chỉ nên load khi nó thực sự cần. Telescope không cần tồn tại lúc bạn mở Neovim để sửa một file .lua; nó chỉ cần khi bạn bấm <leader>ff. Bốn file .lua mở liên tiếp sẽ không kéo theo Telescope, và thời gian start giảm thấy rõ.
Với opts, lazy.nvim tự gọi require("tên-plugin").setup(opts) cho bạn. Nếu bạn viết cả opts và config cùng lúc, setup() có thể chạy hai lần. Chọn một: opts cho cấu hình đơn giản, config khi cần làm gì đó trước hoặc sau setup().
vim.pack: khi nào bỏ lazy.nvim dùng đồ có sẵn
Từ Neovim 0.12, Neovim có plugin manager built-in tên vim.pack. Cú pháp rất ngắn:
-- Neovim 0.12+
vim.pack.add({
"https://github.com/lewis6991/gitsigns.nvim",
})
require("gitsigns").setup()
vim.pack cài plugin vào nvim-data\site\pack\core\opt, ghi phiên bản đã cài vào nvim-pack-lock.json (commit file này vào dotfiles để cài lại đúng bản), và cập nhật bằng :packupdate. Đổi lại, nó cố tình tối giản: không có lazy loading theo event/cmd/keys, không có UI, không tự gọi setup(). Bạn tự lo phần "load khi nào". Cú pháp có thể còn thay đổi giữa các bản, nên luôn mở :help vim.pack đúng với Neovim bạn đang chạy.
| Tiêu chí | lazy.nvim | vim.pack |
|---|---|---|
| Cài đặt | tự bootstrap bằng git clone |
có sẵn từ 0.12 |
| Lazy loading theo event/cmd/keys | có | không, bạn tự xử lý |
| UI quản lý | :Lazy |
không |
| Lockfile | lazy-lock.json |
nvim-pack-lock.json |
| Phụ thuộc bên thứ ba | có | không |
Quy tắc chọn của tôi: config dưới ~10 plugin, bạn thích tối giản và không muốn phụ thuộc repo ngoài thì vim.pack là đủ. Cần lazy loading, profile để đo thời gian start, UI để bật tắt plugin khi debug thì lazy.nvim vẫn thắng. Cấu trúc lua/plugins/*.lua giữ nguyên trong cả hai trường hợp, chỉ lớp "cài và load" ở init.lua thay đổi, nên bạn không bị khoá cứng vào lựa chọn nào.
Bài tập dựng spec cho config của bạn
Làm trực tiếp trong config thật, mở Neovim lại sau mỗi bước.
- Thêm
lazy.nvimbootstrap vào đầuinit.lua(nếu chưa có), chạy:Lazyvà xác nhận UI mở ra. - Thêm
lua/plugins/gitsigns.luasao cho plugin chỉ load khi mở file, không load lúc khởi động. Kiểm tra bằng:Lazyxem cột "Loaded" có trống lúc mới mở. - Thêm
lua/plugins/telescope.luavới hai keybinding như ví dụ trên. Mở Neovim, đừng bấm gì, gõ:Lazyxem Telescope có load chưa. Sau đó bấm<leader>ffvà xem lại. - Viết lại bài 2 bằng
vim.pack(nếu bạn dùng Neovim 0.12+) và ghi ra giấy một thứ bạn mất so với lazy.nvim.
Đáp án và hướng dẫn
Bài 2: spec cần event = { "BufReadPre", "BufNewFile" }. Nếu bạn đặt lazy = false, plugin sẽ load ngay và cột "Loaded" hiện thời gian.
Bài 3: Telescope phải ở trạng thái chưa load cho tới khi bấm key, vì cmd và keys đều là trigger. Nếu nó load sẵn, kiểm tra bạn có vô tình viết lazy = false hoặc gọi require("telescope") ở một file khác không.
Bài 4: dạng đúng là vim.pack.add({ "https://github.com/lewis6991/gitsigns.nvim" }) rồi tự gọi require("gitsigns").setup(). Bạn mất lazy loading theo event, mất UI :Lazy, và phải tự quản lý thời điểm load.
Lỗi hay gặp khi mới cắm plugin manager
- File trong
lua/plugins/không córeturn:setup("plugins")chỉ đọc giá trị trả về của mỗi file. Quênreturn { ... }là plugin im lặng biến mất. - Quên
dependencies: Telescope thiếuplenary.nvimsẽ báo lỗi module not found. Đọc README của plugin, mục "Requirements". lazy = falsecho mọi thứ: config chạy được nhưng start chậm. Dùng:Lazy profileđể xem plugin nào ăn thời gian.- Viết cả
optsvàconfig:setup()chạy hai lần, một số plugin reset state và mất cấu hình. Bỏ một trong hai. - Đường dẫn Windows trong string Lua:
"C:\Users\me"bị hiểu sai vì\U. Dùng/hoặc escape\\. Vớivim.fn.stdpath, bạn không phải lo chuyện này.
Ba lệnh cứu hộ: :Lazy (UI), Ba lệnh cứu hộ: :Lazy(UI để xem trạng thái từng plugin),:Lazy sync(cài và cập nhật theo spec hiện tại),:checkhealth lazy` (kiểm tra môi trường, git, path).
Những quy tắc để spec không phình to
Sau khi cấu hình chạy được, vấn đề tiếp theo không phải là "cài thêm gì" mà là "giữ spec đủ nhỏ để đọc hiểu sau sáu tháng". Vài thói quen của người viết config lâu năm:
- Mỗi plugin một file, tên file trùng tên plugin.
lua/plugins/telescope.luadễ tìm hơn một fileplugins.luadài 400 dòng. - Chỉ khai báo trigger khi có lý do. Nếu bạn dùng plugin đó trong 90% phiên làm việc,
lazy = falsetrung thực hơn là gắnevent = "VeryLazy"cho có vẻ tối ưu. - Ưu tiên
optshơnconfigkhi plugin hỗ trợ, vì lazy.nvim tự merge default và bảng của bạn. Chỉ dùngconfigkhi cần gọi hàm ngoàisetup(). - Đo trước khi tối ưu.
:Lazy profilecho bạn con số thật. Đoán già là cách nhanh nhất để làm config rối mà chẳng nhanh hơn. - Đóng băng phiên bản bằng
lazy-lock.json. Commit file này vào git để máy khác cài đúng phiên bản, tránh "máy tôi chạy được". - Đừng trộn hai plugin manager trong cùng một config. Chọn lazy.nvim hoặc
vim.pack, không chạy song song, vì đường dẫn và thời điểm load sẽ chồng lấn.
Một mẹo nhỏ cho Windows 11: nếu :Lazy sync treo ở bước clone, kiểm tra git --version trong :terminal. Neovim gọi git qua shell mặc định, và một git chưa có trong PATH của PowerShell sẽ làm bước cài đứng im mà không báo lỗi rõ ràng.
Chốt lại và bước sang keybinding
Ba điều cần mang theo:
- Spec của lazy.nvim là một bảng Lua, và mỗi file trong
lua/plugins/phảireturnbảng đó. - Lazy loading đến từ ba trigger chính:
cmd,keys,event(thêmftcho filetype). Không có trigger thì plugin load ngay. vim.packlà hướng đi chính thức của Neovim: nhẹ, không UI, không lazy loading, phù hợp khi bạn muốn config tối giản và tự kiểm soát.
Kiểm tra nhanh phần này:
Lazy.nvim đọc mỗi file trong thư mục plugins qua _____ của file đó.
lazy = false có nghĩa là plugin được _____.
Ba trigger lazy loading phổ biến là _____, keys và _____.
Khi khai báo cả opts và config, hàm setup() sẽ _____.
vim.pack được dùng qua hàm _____ với tham số là danh sách URL git.
Và đây là bộ thẻ để ôn lại sau khi bạn đã tự tay sửa spec:
Front: Plugin spec trong lazy.nvim là gì? Back: Một bảng Lua mô tả plugin: repo, trigger, dependencies, opts hoặc config.
Front: setup("plugins") đọc gì từ mỗi file trong thư mục?
Back: Giá trị return của file, thường là một spec hoặc bảng nhiều spec.
Front: lazy = false làm gì?
Back: Load plugin ngay khi khởi động, bỏ qua lazy loading.
Front: Ba trigger lazy loading chính của lazy.nvim?
Back: cmd, keys và event (thêm ft cho filetype).
Front: opts khác config ở điểm nào?
Back: opts được truyền vào setup(); config là hàm tự chạy do bạn viết.
Front: Vì sao không nên khai báo cả opts và config?
Back: setup() chạy hai lần, dễ reset state và mất cấu hình.
Front: dependencies dùng để làm gì?
Back: Khai báo plugin phụ phải có trước, ví dụ plenary.nvim cho Telescope.
Front: Lazy.nvim cài plugin vào đâu trên Windows 11?
Back: Thư mục lazy bên trong stdpath("data"), tức ~/AppData/Local/nvim-data/lazy.
Front: Lệnh nào cho biết plugin nào làm chậm khởi động?
Back: :Lazy profile.
Front: Lỗi gì xảy ra khi file trong lua/plugins/ thiếu return?
Back: Plugin bị bỏ qua hoàn toàn, không được cài và không báo lỗi.
Front: Cú pháp cài plugin bằng vim.pack?
Back: vim.pack.add({ "https://github.com/user/repo" }), sau đó tự gọi setup().
Front: vim.pack có sẵn từ phiên bản Neovim nào? Back: Từ Neovim 0.12, trước đó chỉ có ở bản nightly.
Front: Bạn mất gì khi chuyển từ lazy.nvim sang vim.pack?
Back: Lazy loading theo event, UI :Lazy, và việc tự quản lý thời điểm load.
Bài tới sẽ dùng chính bộ plugin vừa cài để nói về keybinding, leader key và cách sắp workflow sao cho tay không rời home row: đó là lúc spec của bạn bắt đầu có ích thật sự.