# 📁 Bộ khung init.lua và cách tổ chức file cấu hình trên Windows 11

By 3T1C

## Bộ khung init.lua và cách tổ chức file cấu hình trên Windows 11

Lần trước bạn dừng cuộc ở đúng chỗ này: mở Neovim ra, định config, nhưng không chắc file cấu hình nằm ở đâu và nên bắt đầu từ file nào. Bài này giải quyết đúng khúc đó. Bạn không cần học lại vim motion, chỉ cần dựng đúng khung thư mục và hiểu vì sao nó được tổ chức như vậy, để những bài sau chỉ việc đổ thêm nội dung vào.

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

- In ra được đường dẫn cấu hình chuẩn bằng `stdpath`
- Tạo `init.lua` cùng cấu trúc `lua/config/` và `lua/plugins/`
- Giải thích được `require('config.options')` ánh xạ tới file nào
- Nêu được lý do tách file thay vì nhồi một `init.lua` dài
- Tránh được các bẫy đường dẫn đặc thù trên Windows 11

### Neovim đọc cấu hình ở đâu trên Windows 11

Đừng đoán đường dẫn, hãy để Neovim tự nói:

```vim
:echo stdpath('config')
:echo stdpath('data')
```

| Biến | Ý nghĩa | Đường dẫn trên Windows 11 |
| --- | --- | --- |
| `stdpath('config')` | Nơi chứa `init.lua`, `lua/` | `C:/Users/<tên>/AppData/Local/nvim` |
| `stdpath('data')` | Plugin, shada, swap | `C:/Users/<tên>/AppData/Local/nvim-data` |
| `stdpath('state')` | Trạng thái runtime | Trùng `data` trên Windows |

Hai điểm khiến người mới mất thời gian nhất:

1. Neovim dùng `%LOCALAPPDATA%`, không phải `%APPDATA%`. Nếu bạn tạo `AppData\Roaming\nvim\init.lua` thì Neovim sẽ không bao giờ đọc file đó.
2. Tên thư mục phải đúng là `nvim`, không phải `neovim`, không phải `.config/nvim`.

Vì `stdpath()` trả về đường dẫn phụ thuộc môi trường, hãy luôn dùng nó trong Lua thay vì hardcode `C:/Users/...`. Đó là lý do cấu hình của bạn chạy được trên cả máy đồng nghiệp.

> **Tip:** 
> Trong PowerShell: `explorer $env:LOCALAPPDATA\nvim` mở thẳng thư mục cấu hình. Dùng nhiều lần trong ngày thì đáng để tạo một function.

### init.lua và thứ tự nạp

Neovim tìm file khởi động ở gốc `stdpath('config')`. Nếu cả `init.vim` và `init.lua` cùng tồn tại, Neovim nạp _____ và bỏ qua file còn lại. Vì vậy khi chuyển sang Lua, bạn nên xóa `init.vim` để khỏi bối rối về sau.

`init.lua` đóng vai trò "mục lục", không phải nơi chứa toàn bộ cấu hình:

```lua
-- %LOCALAPPDATA%/nvim/init.lua
require('config.options')  -- lua/config/options.lua
require('config.keymaps')  -- lua/config/keymaps.lua
require('config.lazy')     -- lua/config/lazy.lua (bài sau)
```

Thứ tự này không tùy tiện. Options phải có trước để plugin đọc được giá trị đúng; keymaps trước để leader key tồn tại khi plugin gắn keymap của chúng; plugin manager sau cùng.

```mermaid
flowchart TD
    A["🚀<br/>init.lua"] --> B["⚙️<br/>config.options"]
    B --> C["⌨️<br/>config.keymaps"]
    C --> D["📦<br/>config.lazy"]
```

Ví dụ nội dung `lua/config/options.lua`:

```lua
-- lua/config/options.lua
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.termguicolors = true
```

`vim.opt` tự chuyển đổi kiểu (boolean, list, number) nên an toàn hơn `vim.o` khi bạn không chắc option nhận dạng dữ liệu gì.

> **Note:** 
> Từ Neovim 0.10 trở lên, `vim.opt.clipboard = 'unnamedplus'` dùng được clipboard hệ thống trực tiếp trên Windows. Bản cũ hơn cần provider ngoài như `win32yank`.

**Bài tập 1.** Bạn vừa cài Neovim, chưa có gì trong máy. Viết hai lệnh để xác định chính xác mình phải tạo file ở đâu, và nêu đường dẫn đầy đủ nếu tên user Windows là `nguyen.an`.

**Xem gợi ý**

Chạy `:echo stdpath('config')` và `:echo stdpath('data')`. Với user `nguyen.an`, cấu hình nằm ở `C:/Users/nguyen.an/AppData/Local/nvim`, dữ liệu ở `C:/Users/nguyen.an/AppData/Local/nvim-data`. Đừng gõ tay đường dẫn này vào config; dùng `stdpath()` trong Lua.

### Tách file: lua/, require và runtimepath

Đây là phần cốt lõi. `require` không biết gì về `init.lua`; nó tìm module trong các thư mục `lua/` nằm trên _____. Nghĩa là thư mục `lua/` ngay dưới `stdpath('config')` đã tự động nằm trong tầm với.

Cấu trúc nên dựng:

```tree
%LOCALAPPDATA%/nvim/
  init.lua
  lua/
    config/
      options.lua
      keymaps.lua
      lazy.lua
    plugins/
```

Quy tắc ánh xạ rất máy móc, không có phép thuật:

- `require('config.options')` nạp file `lua/config/options.lua`
- Dấu `.` thay cho `/`, bỏ đuôi `.lua`
- Base path là `lua/`, không phải thư mục gốc

Hai hệ quả thực tế:

- Sửa nội dung một module đã `require` rồi `:source $MYVIMRC` sẽ **không** nạp lại module đó, vì Lua cache trong `package.loaded`. Muốn thấy thay đổi, restart Neovim, hoặc `:luafile %` khi đang mở chính file đó.
- Nếu `require` báo lỗi "module not found", gần như luôn là sai đường dẫn thư mục, không phải sai cú pháp Lua.

**Bài tập 2.** Tạo `lua/config/keymaps.lua` chứa một keymap lưu file bằng `<leader>w`, rồi nạp nó từ `init.lua`.

**Xem gợi ý**

```lua
-- lua/config/keymaps.lua
vim.keymap.set('n', '<leader>w', '<cmd>write<cr>', { desc = 'Lưu file' })
```

Trong `init.lua` thêm `require('config.keymaps')`. Nếu chưa đặt leader key, `<leader>` mặc định là `\`; bài 1.3 sẽ đặt `vim.g.mapleader` tử tế. Lưu ý `vim.g.mapleader` phải được gán trước khi keymap được tạo.

### Vì sao không nhồi hết vào một init.lua

Bạn hoàn toàn có thể viết một file 1500 dòng và nó vẫn chạy. Nhưng khi lỗi xảy ra lúc 23h trước deadline, bạn sẽ trả giá:

- **Khoanh vùng lỗi nhanh.** Neovim báo lỗi kèm số dòng; nếu lỗi ở `lua/config/options.lua` thì phạm vi nghi ngờ chỉ còn vài chục dòng.
- **Git diff sạch.** Thêm Telescope không làm diff loang ra toàn bộ file, nên review và revert dễ hơn nhiều.
- **Thứ tự nạp tường minh.** `init.lua` đọc như mục lục, ai vào sau cũng hiểu cái gì chạy trước.
- **Tách theo plugin.** `lua/plugins/` cho phép mỗi plugin một file, đúng cách `lazy.nvim` sẽ import ở bài sau.
- **Dễ vô hiệu hóa.** Comment một dòng `require` là tắt cả một nhóm cấu hình, thay vì đi xóa từng block.

### Những bẫy đường dẫn riêng của Windows 11

- Trong string Lua, `\` là ký tự escape. `"C:\Users"` sẽ lỗi vì `\U` không hợp lệ. Dùng `/` (`"C:/Users"`) hoặc escape kép (`"C:\\Users"`).
- Tên user có dấu tiếng Việt hoặc dấu cách có thể làm hỏng một số tool biên dịch bên ngoài. Nếu gặp lỗi lạ khi cài LSP server, kiểm tra đường dẫn trước khi nghi plugin.
- File cấu hình nên lưu với LF. Trộn CRLF thường không sao, nhưng script shell gọi từ Neovim trên WSL thì có sao.

### Ba lỗi làm hỏng khung ngay từ đầu

1. **Tạo config trong `AppData\Roaming`.** Neovim không đọc ở đó. Luôn xác nhận bằng `:echo stdpath('config')` trước khi viết dòng Lua đầu tiên.
2. **Đặt `init.lua` vào trong `lua/`.** Nó phải nằm ở gốc `stdpath('config')`. `lua/init.lua` chỉ là một module bình thường, không được nạp tự động.
3. **Sửa file rồi `:source $MYVIMRC` và tưởng nó chưa ăn.** Với module đã cache, bạn phải restart. Đây là hiểu nhầm tốn thời gian nhất của người mới chuyển từ `init.vim` sang Lua.

Một quy tắc ngón tay: mỗi file dưới 100 dòng, mỗi file một trách nhiệm. Khi một file vượt 100 dòng, đó là tín hiệu nên tách.

### Chốt lại

Bạn vừa có bộ khung chạy được: `init.lua` làm mục lục, `lua/config/` chứa options và keymaps, `lua/plugins/` để trống chờ plugin. Đường dẫn luôn lấy từ `stdpath('config')`, và mọi thứ nối với nhau qua `require` trên các thư mục `lua/` trong `runtimepath`.

Bài tiếp theo đổ phần thân vào khung đó: cài `lazy.nvim`, viết spec plugin đầu tiên và bật lazy loading, cùng hướng mở rộng sang `vim.pack`.

Front: Lệnh nào in ra đường dẫn thư mục cấu hình Neovim?
Back: `:echo stdpath('config')`

Front: `stdpath('config')` trỏ tới đâu trên Windows 11?
Back: `%LOCALAPPDATA%/nvim`, thường là `C:/Users/<tên>/AppData/Local/nvim`

Front: `stdpath('data')` trên Windows 11 là gì?
Back: `%LOCALAPPDATA%/nvim-data`, nơi chứa plugin, shada và swap file

Front: `%APPDATA%` (Roaming) có phải nơi Neovim đọc config không?
Back: Không. Đây là bẫy phổ biến; Neovim dùng `%LOCALAPPDATA%`

Front: Nếu cả `init.lua` và `init.vim` cùng tồn tại thì Neovim nạp file nào?
Back: `init.lua`, và bỏ qua `init.vim`

Front: `require('config.options')` nạp file nào?
Back: `lua/config/options.lua` bên trong `stdpath('config')`

Front: `require` tìm module Lua dựa trên cơ sở nào?
Back: Các thư mục `lua/` nằm trên `runtimepath`

Front: Biến nào trỏ tới file cấu hình đang chạy?
Back: `$MYVIMRC`; dùng `:source $MYVIMRC` để nạp lại

Front: Vì sao không viết `C:\Users` trong string Lua?
Back: `\` là ký tự escape trong Lua; dùng `/` hoặc escape kép `\\`

Front: Thứ tự `require` hợp lý trong `init.lua`?
Back: options, rồi keymaps, rồi plugin manager (lazy)

Front: Vì sao tách config thành nhiều file nhỏ?
Back: Dễ khoanh vùng lỗi, git diff sạch, thứ tự nạp rõ ràng, dễ tắt từng nhóm

Front: `package.loaded` ảnh hưởng gì khi sửa file trong `lua/`?
Back: Module đã require bị cache, phải restart Neovim hoặc `:luafile %` mới thấy thay đổi

Front: Thư mục `lua/plugins/` trong khung dùng để làm gì?
Back: Chứa spec plugin, mỗi plugin một file, để `lazy.nvim` import ở bài sau

Bạn còn nhớ: `init.lua` phải nằm ở gốc, còn đường dẫn cấu hình thì luôn hỏi Neovim qua _____ thay vì gõ tay. `require('config.keymaps')` ánh xạ tới _____, và `require` chỉ nạp một lần nhờ cache trong _____.

---

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