# 🌳 Treesitter: cú pháp, highlight và folding

By 3T1C

Bài trước bạn đã có Telescope để nhảy tới bất kỳ file hay dòng grep nào trong dự án. Nhưng khi đã đứng đúng chỗ, thứ quyết định bạn đọc code nhanh hay chậm là **khả năng nhìn ra cấu trúc**: đâu là thân hàm, đâu là block, đâu là chuỗi, đâu là comment. Treesitter là lớp đó, và nó cũng là nền móng cho folding theo cú pháp mà bạn sẽ dùng suốt phần còn lại của khóa.

Sau bài này bạn sẽ:

- Giải thích được Treesitter làm gì khác syntax regex của Vim.
- Cài `nvim-treesitter` trên Windows 11 với compiler đúng ngay từ đầu.
- Bật highlight và kiểm chứng bằng `:InspectTree` / `:Inspect`.
- Bật folding theo cấu trúc cú pháp.
- Xử lý được ba lỗi quen thuộc: parser chưa cài, build lỗi, ABI lệch sau khi update Neovim.

## Vì sao parser quan trọng hơn regex

Syntax file của Vim cổ điển là một tập regex chạy theo từng dòng. Nó không biết gì về ngữ cảnh, nên chỉ cần một dấu backtick trong chuỗi JavaScript hoặc một `#` trong chuỗi Python là phần còn lại của file bị tô sai. Với file TypeScript vài nghìn dòng, lỗi này lan ra rất nhanh và bạn mất niềm tin vào màu sắc.

Treesitter chạy một **parser thật** (viết bằng C, sinh ra từ grammar của từng ngôn ngữ) và trả về một AST, tức là cây cú pháp của buffer. Cây này được cập nhật tăng dần (incremental) sau mỗi lần bạn gõ, nên chi phí gần như không đổi dù file lớn. Từ cây đó Neovim suy ra ba thứ: highlight group cho từng node, vùng indent, và biên của fold.

> **Note:** 
> `nvim-treesitter` có hai branch với API khác nhau. Branch `master` dùng `require("nvim-treesitter.configs").setup({...})`, chỉ cần một C compiler để build parser. Branch `main` dùng API mới (`require("nvim-treesitter").setup()`, `vim.treesitter.start()`) và cần thêm `tree-sitter` CLI. Bài này đi theo `master` vì phần lớn config đang chạy thực tế dùng nó; nếu bạn theo `main`, hãy đọc README của repo trước khi copy config.

## Chuẩn bị compiler trên Windows 11 trước khi cài

Đây là chỗ 90% người mới bỏ cuộc: cài plugin xong, chạy `:TSInstall python` và nhận `No C compiler found`. Parser phải được **biên dịch từ source trên máy bạn**, nên cần một C compiler có trong `PATH`.

Ba lựa chọn thực dụng, chọn một:

- **Visual Studio Build Tools** với workload "Desktop development with C++" (nặng nhưng ổn định nhất).
- **LLVM/Clang** qua `winget install LLVM.LLVM`.
- **Zig** (`winget install zig.zig`) làm compiler C thay thế, nhẹ và không cần Visual Studio.

Sau đó khai báo thứ tự compiler cho `nvim-treesitter`. Dòng này phải chạy **trước** khi cài parser, nên đặt ngay đầu file config của plugin:

```lua
-- lua/plugins/treesitter.lua
require("nvim-treesitter.install").compilers = { "clang", "gcc", "zig" }

return {
  {
    "nvim-treesitter/nvim-treesitter",
    branch = "master",
    lazy = false,
    build = ":TSUpdate",
    config = function()
      require("nvim-treesitter.configs").setup({
        -- parser nào cần có sẵn ngay khi clone config sang máy mới
        ensure_installed = {
          "lua", "vim", "vimdoc", "python",
          "typescript", "tsx", "json", "markdown", "markdown_inline",
        },
        highlight = { enable = true },
        indent = { enable = true },
      })
    end,
  },
}
```

Hai điểm cần hiểu chứ không chỉ copy:

- `build = ":TSUpdate"` là hook của lazy.nvim. Mỗi lần plugin được cập nhật, lệnh này chạy lại và rebuild parser đã cài. Thiếu nó, bạn sẽ gặp lỗi ABI về sau.
- `ensure_installed` nhận **tên parser**, không phải tên filetype. File `.tsx` dùng parser `tsx`, không phải `typescriptreact`; muốn highlight markdown đầy đủ thì phải có cả `markdown_inline`.

> **Important:** 
> Plugin phải nằm trong thư mục mà bạn có quyền ghi, mặc định với lazy.nvim là `%LOCALAPPDATA%\nvim-data\lazy`. Nếu bạn trỏ `root` sang ổ đĩa chỉ đọc hoặc thư mục `Program Files`, bước build sẽ thất bại với `Permission denied`.

**Gợi ý và đáp án**

Trong cấu hình capstone, bạn cần thêm parser `toml` và `yaml` (dùng cho file cấu hình dự án) vào `ensure_installed`. Thêm hai chuỗi đó vào danh sách, lưu file, chạy `:Lazy sync`, rồi mở một file `docker-compose.yml` để kiểm tra. Bạn **không** cần `:TSInstall` thủ công vì `build` + `ensure_installed` đã lo phần đó. Nhưng nếu parser đã cài từ trước và bạn chỉ sửa danh sách, hãy chạy `:TSUpdate` để chắc chắn mọi thứ đồng bộ.

## Bật highlight và kiểm chứng bằng cây cú pháp

Với `highlight = { enable = true }`, Treesitter tự bật cho buffer có filetype tương ứng và có parser. Neovim 0.10 trở lên còn có API trực tiếp để bạn bật tay khi cần, ví dụ cho một file `.env` chưa được nhận diện:

```lua
vim.api.nvim_create_autocmd("FileType", {
  pattern = "env",
  callback = function()
    vim.treesitter.start(0, "bash")
  end,
})
```

Cách kiểm chứng đáng giá nhất là nhìn thẳng vào cây:

- `:InspectTree` mở cửa sổ cây cú pháp. Đặt con trỏ lên một node, bạn thấy tên node và range của nó. Đây là công cụ bạn sẽ dùng lại khi viết query hoặc debug folding.
- `:Inspect` cho biết highlight group tại ký tự dưới con trỏ. Nếu nó trả về group do Treesitter đặt, parser đang chạy đúng.
- `:checkhealth nvim-treesitter` liệt kê parser đã cài, parser lỗi, và compiler tìm thấy được.

Một mẹo nhỏ: so sánh trước và sau bằng `:syntax off` rồi `:e`. Nếu màu không đổi, rất có thể bạn đang nhìn syntax regex cũ chứ không phải Treesitter.

## Folding theo cấu trúc cú pháp

Đây là phần khiến Treesitter đáng giá trong công việc thật: gập đúng thân hàm, đúng block `if`, đúng object literal, thay vì gập theo indent (dễ sai khi code trộn tab và space).

```lua
-- đặt trong phần options của init.lua
vim.opt.foldmethod = "expr"
vim.opt.foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.opt.foldlevel = 99
```

Giải thích từng dòng:

- `foldmethod = "expr"` bắt Neovim đánh giá `foldexpr` cho từng dòng để quyết định mức fold.
- `foldexpr` trỏ vào hàm của Treesitter (có sẵn từ Neovim 0.10). Hàm này tra cây cú pháp và trả về độ sâu fold của dòng hiện tại.
- `foldlevel = 99` lớn hơn mọi độ sâu thực tế, nên file mở ra vẫn phẳng, không bị gập sẵn. Bạn chủ động gập bằng `za` (một khối), `zM` (tất cả), `zR` (mở tất cả), `zc`/`zo`.

Với file Lua hay Python, `za` trên dòng `function` sẽ gập trọn thân hàm, kể cả khi trong thân có chuỗi nhiều dòng làm indent sai lệch. Đó chính là khác biệt so với `foldmethod = "indent"`.

**Gợi ý và đáp án**

Mở một file Python trong dự án, đặt con trỏ lên dòng `def`, gõ `za` và thấy cả thân hàm gập lại. Nếu `za` không làm gì, kiểm tra theo thứ tự: `:set foldmethod?` phải trả về `expr`; `:set foldexpr?` phải chứa `v:lua.vim.treesitter.foldexpr()`; `:TSInstallInfo` phải cho thấy `python` ở trạng thái `installed`. Thiếu bất kỳ điều nào trong ba điều đó là đủ để folding im lặng không hoạt động.

## Khi parser chưa cài hoặc build lỗi

| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
| --- | --- | --- |
| Không có highlight, `:Inspect` trả group cũ | parser chưa cài | `:TSInstall python` |
| `No C compiler found` | chưa có clang/gcc/zig trong `PATH` | cài compiler, khai báo `compilers` |
| `incompatible ABI version` | parser build bằng Neovim bản cũ, sau đó bạn update Neovim | `:TSUpdate` để rebuild toàn bộ |
| `Permission denied` khi build | thư mục plugin không ghi được | chuyển lazy root về `%LOCALAPPDATA%` |
| Neovim chậm khi mở file rất lớn | parse và highlight tốn tài nguyên | tắt `highlight` cho filetype đó |

Lỗi ABI là lỗi Windows hay gặp nhất vì nhiều bạn cài Neovim qua zip rồi update bằng cách giải nén đè. Parser cũ vẫn nằm trong thư mục plugin, ABI của Neovim mới thì khác. `:TSUpdate` một lần là xong, và hook `build = ":TSUpdate"` trong config ở trên chính là để việc đó tự xảy ra.

## Những điều nên nhớ về Treesitter

- **Đừng cài cả trăm parser.** Mỗi parser là một lần biên dịch và một thư mục trong config. Chỉ giữ những ngôn ngữ bạn thật sự mở trong tuần.
- **`:checkhealth nvim-treesitter` là bước đầu tiên** khi highlight có vấn đề, không phải bước cuối. Nó nói rõ parser nào lỗi và compiler nào được tìm thấy.
- **Sai tên parser là lỗi thầm lặng.** `:TSInstall tsx` và `:TSInstall typescriptreact` là hai chuyện khác nhau; cái sau sẽ báo không tìm thấy ngôn ngữ.
- **Đặt `vim.opt.foldtext = ""`** nếu bạn muốn dòng tiêu đề của fold hiển thị text gốc kèm highlight thay vì chuỗi rút gọn mặc định của Vim.
- **Sau mỗi lần nâng cấp Neovim, chạy `:TSUpdate`** như một phản xạ. Nó mất vài chục giây và tiết kiệm cả buổi debug.

## Tóm lại

Treesitter cho Neovim khả năng đọc cấu trúc chứ không chỉ so khớp mẫu: highlight đúng ngữ cảnh, indent hợp lý, folding theo AST. Trên Windows 11, toàn bộ độ khó nằm ở chỗ chuẩn bị compiler trước khi cài và rebuild parser sau khi nâng cấp Neovim. Làm đúng hai việc đó, phần còn lại chỉ là thêm tên parser vào `ensure_installed`.

Bài tiếp theo là checkpoint của module: bạn sẽ ghép LSP, Telescope và Treesitter lại trong một buổi thực hành ngắn, kiểm tra từng lớp bằng `:checkhealth` và `:InspectTree` để chắc chắn cấu hình capstone chạy ổn trước khi bước sang phần Git và terminal.

Front: Treesitter hiểu mã nguồn bằng cách nào thay vì regex?
Back: Chạy parser của từng ngôn ngữ, sinh AST và cập nhật tăng dần theo buffer.

Front: Vì sao highlight bằng syntax regex của Vim hay tô sai?
Back: Nó chỉ so khớp pattern theo từng dòng, không biết ngữ cảnh như chuỗi hay comment.

Front: Điều kiện bắt buộc để build parser trên Windows 11 là gì?
Back: Một C compiler có trong PATH: MSVC Build Tools, clang, hoặc zig.

Front: Tuỳ chọn nào khai báo thứ tự compiler cho nvim-treesitter?
Back: require("nvim-treesitter.install").compilers = { "clang", "gcc", "zig" }.

Front: Lệnh cài parser cho một ngôn ngữ?
Back: :TSInstall <tên parser>, ví dụ :TSInstall python.

Front: Gặp lỗi incompatible ABI version sau khi nâng cấp Neovim thì làm gì?
Back: Chạy :TSUpdate để rebuild toàn bộ parser đã cài.

Front: Hook nào của lazy.nvim giữ parser luôn khớp với Neovim?
Back: build = ":TSUpdate" trong spec của nvim-treesitter.

Front: Lệnh xem cây cú pháp của buffer hiện tại?
Back: :InspectTree.

Front: Lệnh kiểm tra highlight group tại ký tự dưới con trỏ?
Back: :Inspect.

Front: foldexpr cần đặt bằng gì để folding theo cú pháp?
Back: v:lua.vim.treesitter.foldexpr().

Front: foldmethod phải là gì để foldexpr có tác dụng?
Back: expr.

Front: foldmethod phải đặt bằng gì thì foldexpr mới được dùng?
Back: "expr", ví dụ vim.opt.foldmethod = "expr".

Front: Parser đã build nằm ở đâu, và vì sao xoá data dir lại mất parser?
Back: Nằm trong thư mục data của Neovim, nên xoá data dir thì phải :TSInstall lại.

Front: Vì sao chỉ nên bật highlight và indent của Treesitter cho ngôn ngữ đã có parser?
Back: Vì module đó thay cơ chế highlight/indent của Vim, filetype không có parser sẽ mất highlight hoặc lỗi.

## Chốt lại trước khi sang checkpoint

Ba điều đáng mang theo từ bài này:

- Treesitter không phải một plugin highlight, nó là bộ parser sinh ra AST. Mọi thứ khác (highlight, folding, indent, text object) chỉ là lớp phủ lên AST đó, nên _____.
- Trên Windows 11, lỗi thường gặp nhất không nằm ở config Lua mà ở khâu build: thiếu C compiler trong PATH hoặc compiler bị Neovim chọn sai. Kiểm tra bằng _____ trước khi sửa bất kỳ dòng config nào.
- Folding theo cú pháp chỉ hoạt động khi hai tuỳ chọn đi cùng nhau: `foldmethod = "expr"` và `foldexpr = v:lua.vim.treesitter._____`. Thiếu một trong hai thì fold vẫn hoạt động nhưng theo kiểu mặc định của Vim.

Bài tiếp theo là checkpoint của module: bạn sẽ tự tay ghép LSP, Telescope và Treesitter vào cùng một cấu hình capstone, chạy thử trên một dự án thật, rồi gỡ các lỗi còn sót trước khi bước sang phần Git và terminal.

Front: Treesitter hiểu mã nguồn bằng cách nào thay vì regex?
Back: Chạy parser của từng ngôn ngữ, sinh AST và cập nhật tăng dần theo buffer.

Front: Vì sao highlight bằng syntax regex của Vim hay tô sai?
Back: Nó chỉ so khớp pattern theo từng dòng, không biết ngữ cảnh như chuỗi hay comment.

Front: Điều kiện bắt buộc để build parser trên Windows 11 là gì?
Back: Một C compiler có trong PATH: MSVC Build Tools, clang, hoặc zig.

Front: Tuỳ chọn nào khai báo thứ tự compiler cho nvim-treesitter?
Back: require("nvim-treesitter.install").compilers = { "clang", "gcc", "zig" }.

Front: Lệnh cài parser cho một ngôn ngữ?
Back: :TSInstall <tên parser>, ví dụ :TSInstall python.

Front: Gặp lỗi incompatible ABI version sau khi nâng cấp Neovim thì làm gì?
Back: Chạy :TSUpdate để rebuild toàn bộ parser đã cài.

Front: Hook nào của lazy.nvim giữ parser luôn khớp với Neovim?
Back: build = ":TSUpdate" trong spec của nvim-treesitter.

Front: Lệnh xem cây cú pháp của buffer hiện tại?
Back: :InspectTree.

Front: Lệnh kiểm tra highlight group tại ký tự dưới con trỏ?
Back: :Inspect.

Front: foldexpr cần đặt bằng gì để folding theo cú pháp?
Back: v:lua.vim.treesitter.foldexpr().

Front: foldmethod phải đặt bằng gì thì foldexpr mới được dùng?
Back: "expr", ví dụ vim.opt.foldmethod = "expr".

Front: Parser đã build nằm ở đâu, và vì sao xoá data dir lại mất parser?
Back: Nằm trong thư mục data của Neovim, nên xoá data dir thì phải :TSInstall lại.

Front: Vì sao chỉ nên bật highlight và indent của Treesitter cho ngôn ngữ đã có parser?
Back: Vì module đó thay cơ chế highlight/indent của Vim, filetype không có parser sẽ mất highlight hoặc lỗi.

---

## 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)
  - [⌨️ 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) — this lesson
  - [✅ 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)
