# 📦 Quản lý plugin với lazy.nvim và hướng mở rộng với vim.pack

By 3T1C

> Sign in to view the full content of this lesson.

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-data` cò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.pack` có 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:

```lua
-- ~/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ên `vim.loop` thành `vim.uv`. Dùng cách này thì config chạy được ở cả hai bản.
- `vim.fn.system({...})` gọi `git clone` vớ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ào `runtimepath` thì `require("lazy")` mới tìm thấy.
- `require("lazy").setup("plugins")` nghĩa là import toàn bộ file trong `lua/plugins/`, mỗi file `return` một spec (hoặc một mảng spec).

> **Warning:** 
> Bootstrap cần `git` có trong `PATH`. Trên Windows 11, nếu bạn cài Git nhưng chưa thêm vào `PATH`, đ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 --version` ngay 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
-- lua/plugins/gitsigns.lua
return {
  "lewis6991/gitsigns.nvim",
  event = { "BufReadPre", "BufNewFile" },
  opts = { signs = { add = { text = "│" } } },
}
```

```lua
-- 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:

```lua
-- 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.

1. Thêm `lazy.nvim` bootstrap vào đầu `init.lua` (nếu chưa có), chạy `:Lazy` và xác nhận UI mở ra.
2. Thêm `lua/plugins/gitsigns.lua` sao cho plugin chỉ load khi mở file, không load lúc khởi động. Kiểm tra bằng `:Lazy` xem cột "Loaded" có trống lúc mới mở.
3. Thêm `lua/plugins/telescope.lua` với hai keybinding như ví dụ trên. Mở Neovim, đừng bấm gì, gõ `:Lazy` xem Telescope có load chưa. Sau đó bấm `<leader>ff` và xem lại.
4. 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ên `return { ... }` là plugin im lặng biến mất.
- **Quên `dependencies`**: Telescope thiếu `plenary.nvim` sẽ báo lỗi module not found. Đọc README của plugin, mục "Requirements".
- **`lazy = false` cho 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ả `opts` và `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ới `vim.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.lua` dễ tìm hơn một file `plugins.lua` dà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 = false` trung thực hơn là gắn `event = "VeryLazy"` cho có vẻ tối ưu.
- **Ưu tiên `opts` hơn `config`** khi plugin hỗ trợ, vì lazy.nvim tự merge default và bảng của bạn. Chỉ dùng `config` khi cần gọi hàm ngoài `setup()`.
- **Đo trước khi tối ưu.** `:Lazy profile` cho 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:

1. Spec của lazy.nvim là một bảng Lua, và mỗi file trong `lua/plugins/` phải `return` bảng đó.
2. Lazy loading đến từ ba trigger chính: `cmd`, `keys`, `event` (thêm `ft` cho filetype). Không có trigger thì plugin load ngay.
3. `vim.pack` là 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ự.

---

## Contents

Part of [🛠️ Config neovim trên win 11](https://everwise.study/courses/config-neovim-tren-win-11.md)

- **🧱 Nền tảng cấu hình bằng Lua và plugin manager**
  - [📁 Bộ khung init.lua và cách tổ chức file cấu hình trên Windows 11](https://everwise.study/courses/config-neovim-tren-win-11/bo-khung-init-lua-tren-windows-11.md)
  - [📦 Quản lý plugin với lazy.nvim và hướng mở rộng với vim.pack](https://everwise.study/courses/config-neovim-tren-win-11/quan-ly-plugin-lazy-nvim-va-vim-pack.md) (sign-in required) — this lesson
  - [⌨️ Keybinding, leader key và workflow làm việc hiệu quả](https://everwise.study/courses/config-neovim-tren-win-11/keybinding-leader-key-va-workflow.md)
  - [✅ Checkpoint: Nền tảng cấu hình Lua và plugin manager](https://everwise.study/courses/config-neovim-tren-win-11/checkpoint-nen-tang-cau-hinh-lua.md)
- **🧠 Đưa Neovim lên mức IDE: LSP, Telescope, Treesitter**
  - [🔍 LSP, autocomplete và diagnostics](https://everwise.study/courses/config-neovim-tren-win-11/lsp-autocomplete-va-diagnostics.md)
  - [🔭 Telescope: tìm file và grep trong dự án](https://everwise.study/courses/config-neovim-tren-win-11/telescope-tim-file-va-grep.md)
  - [🌳 Treesitter: cú pháp, highlight và folding](https://everwise.study/courses/config-neovim-tren-win-11/treesitter-cu-phap-va-folding.md)
  - [✅ Checkpoint: LSP, Telescope và Treesitter](https://everwise.study/courses/config-neovim-tren-win-11/checkpoint-lsp-telescope-treesitter.md)
- **🚀 Capstone: cấu hình hoàn chỉnh dùng cho công việc thật**
  - [🌿 Tích hợp Git và terminal trong Neovim](https://everwise.study/courses/config-neovim-tren-win-11/tich-hop-git-va-terminal.md)
  - [🩺 Khắc phục lỗi đặc thù trên Windows 11](https://everwise.study/courses/config-neovim-tren-win-11/khac-phuc-loi-dac-thu-tren-windows-11.md)
  - [🏗️ Capstone: đóng gói cấu hình Neovim cho công việc thật](https://everwise.study/courses/config-neovim-tren-win-11/capstone-dong-goi-cau-hinh-neovim.md) (premium content)
  - [✅ Checkpoint: Capstone và xử lý sự cố trên Windows](https://everwise.study/courses/config-neovim-tren-win-11/checkpoint-capstone-windows.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/config-neovim-tren-win-11/cheatsheet.md) (premium content)
