Bộ khung init.lua và cách tổ chức file cấu hình trên Windows 11
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.luacùng cấu trúclua/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.luadà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:
: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:
- Neovim dùng
%LOCALAPPDATA%, không phải%APPDATA%. Nếu bạn tạoAppData\Roaming\nvim\init.luathì Neovim sẽ không bao giờ đọc file đó. - Tên thư mục phải đúng là
nvim, không phảineovim, 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\nvimmở 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:
-- %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.
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/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:
%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 filelua/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 đã
requirerồi:source $MYVIMRCsẽ không nạp lại module đó, vì Lua cache trongpackage.loaded. Muốn thấy thay đổi, restart Neovim, hoặc:luafile %khi đang mở chính file đó. - Nếu
requirebá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/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.luathì 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áchlazy.nvimsẽ import ở bài sau. - Dễ vô hiệu hóa. Comment một dòng
requirelà 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ì\Ukhô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
- 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. - Đặt
init.luavào tronglua/. Nó phải nằm ở gốcstdpath('config').lua/init.luachỉ là một module bình thường, không được nạp tự động. - Sửa file rồi
:source $MYVIMRCvà 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.vimsang 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 _____.