🩺 Khắc phục lỗi đặc thù trên Windows 11

By 3T1C

Cấu hình đã chạy, giờ là lúc Windows kéo bạn lại

Ở bài trước bạn đã gắn Git và terminal vào Neovim, và ngay khi terminal gọi pwsh thay vì cmd.exe, một loạt lỗi "chỉ có trên Windows" bắt đầu lộ ra. Bài này là bộ đồ nghề chẩn đoán: nhận diện triệu chứng, khoanh vùng nguyên nhân, sửa bằng đúng một dòng config.

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

  • Đọc được :checkhealth và :messages để khoanh vùng lỗi thay vì thử ngẫu nhiên.
  • Xử lý path có dấu cách trong Lua config và trong command build.
  • Phân biệt lỗi "thiếu quyền thực thi" trên Windows với chmod trên Linux.
  • Chọn được shell mặc định hợp lý và biết giá phải trả khi đổi.
  • Chuẩn hoá encoding, line ending, clipboard và toolchain compiler cho plugin.

Chẩn đoán theo thứ tự, đừng đoán mò

Trước khi sửa, hãy xác định lỗi nằm ở tầng shell (lệnh :!, build plugin) hay tầng Neovim (LSP, parser, UI). Hai tầng này có công cụ chẩn đoán khác nhau.

flowchart TD
    Bug["🐞 Lỗi xuất hiện"] --> Q1{"Lỗi ở lệnh shell<br/>hay trong Neovim?"}
    Q1 -->|"lệnh :! hoặc build"| Shell["🖥️ Kiểm tra shell, PATH,<br/>dấu cách trong path"]
    Q1 -->|"LSP, parser, UI"| Health["🩺 :checkhealth + :messages"]
    Shell --> Fix["Sửa một biến, chạy lại"]
    Health --> Fix

Đường dẫn có dấu cách: lỗi im lặng khó thấy nhất

Windows đặt config và data của Neovim dưới C:\Users\<tên user>\AppData\Local\.... Nếu tên user có dấu cách (ví dụ Nguyen Van A), mọi command string ghép tay sẽ bị shell tách thành nhiều tham số, và lỗi hiện ra rất mơ hồ: "file not found", "no such directory", build fail không log.

Ba cách xử lý, theo thứ tự ưu tiên:

  1. Truyền args dạng list cho vim.system({ "gcc", path }), không ghép chuỗi.
  2. Nếu buộc phải ghép chuỗi, bọc bằng vim.fn.shellescape(path).
  3. Giữ project trong thư mục sạch dấu cách, ví dụ C:\dev\du-an-web, và không hardcode C:\Users\... ở bất kỳ đâu. Dùng vim.fn.stdpath("data") và vim.fs.joinpath.

Note: Neovim nhận / ở hầu hết API, nên trong Lua hãy viết "C:/dev/du-an-web" thay vì escape \\. Ít lỗi hơn và dễ copy sang máy khác.

Thử ngay: chạy :lua print(vim.fn.stdpath("data")). Nếu kết quả có dấu cách, ghi lại và kiểm tra xem plugin nào đang nhận path này qua command string.

Gợi ý và đáp án

Xử lý chuẩn là đổi các chỗ ghép chuỗi sang vim.system với list args, hoặc bọc bằng shellescape. Đồng thời chuyển project sang C:\dev để giảm bề mặt lỗi.

Quyền thực thi: Windows không có chmod

Windows không có bit thực thi, nên "thiếu quyền" thực chất là một trong ba chuyện sau:

  • Binary không nằm trong PATH, hoặc thiếu đuôi trong PATHEXT. Kiểm tra bằng vim.fn.executable("rg") == 1.
  • Script .ps1 bị ExecutionPolicy chặn.
  • File tải từ Internet bị Mark of the Web. Gỡ bằng Unblock-File .\file.exe trong PowerShell.

Chỉ khi chạy trong WSL hoặc Git Bash bạn mới thật sự cần chmod +x.

Shell mặc định: giữ cmd.exe hay đổi sang pwsh

Neovim lấy shell từ option shell (mặc định cmd.exe) và dùng nó cho :!, system(), và mọi plugin gọi lệnh ngoài. Đổi shell là thay đổi có rủi ro, vì shellcmdflag, shellquote, shellxquote phải đi kèm đồng bộ:

-- Chỉ đổi khi thật cần. Test lại :!echo hi và :Lazy build sau khi đổi.
vim.o.shell = vim.fn.exepath("pwsh")
vim.o.shellcmdflag = "-NoLogo -NoProfile -ExecutionPolicy Bypass -Command"
vim.o.shellquote = ""
vim.o.shellxquote = ""
vim.o.shellredir = ">%s 2>&1"

Lời khuyên của tôi: nếu terminal tích hợp đã chạy pwsh ổn, hãy để shell là cmd.exe cho các lệnh một dòng. Bạn có shell đẹp trong terminal plugin mà không phá :! của plugin khác.

Thử ngay: in :lua print(vim.o.shell, vim.o.shellcmdflag, vim.o.shellxquote) trước và sau khi đổi, để thấy rõ cái gì thay đổi.

Gợi ý và đáp án

Trước: cmd.exe /s /c ". Sau: đường dẫn pwsh.exe với flag -Command. Nếu shellxquote còn là " sau khi đổi, quoting sẽ sai và lệnh có path sẽ vỡ.

Encoding, line ending và ký tự lạ

Neovim luôn xử lý nội bộ bằng UTF-8. fileencoding chỉ nói cách đọc/ghi file, còn fileformats quyết định LF hay CRLF.

vim.opt.fileencoding = "utf-8"
vim.opt.fileformats = "unix,dos" -- ưu tiên LF cho file mới

Hai cái bẫy hay gặp: config copy từ Vim có set termencoding=utf-8 sẽ báo lỗi vì Neovim đã bỏ option này; và file xuất từ tool legacy (CP1258, UTF-16) hiện ký tự lạ, sửa bằng :e ++enc=cp1258. Nếu chỉ icon và emoji bị ô vuông, đó là font chứ không phải encoding: dùng Windows Terminal kèm Nerd Font.

Clipboard: có sẵn, chỉ cần bật

Bản Neovim chính thức cho Windows đã có clipboard tích hợp, không cần win32yank. Bạn chỉ cần:

vim.opt.clipboard = "unnamedplus"

win32yank.exe chỉ cần khi chạy Neovim trong WSL hoặc qua SSH. Kiểm tra bằng :checkhealth, mục clipboard. Nếu provider báo OK mà copy vẫn không sang app khác, nguyên nhân thường là môi trường terminal chứ không phải config.

Thử ngay: mở một file bất kỳ, yy rồi sang Notepad dán thử. Nếu dán ra đúng nội dung, clipboard đã thông. Nếu không, chạy :checkhealth và đọc mục clipboard.

Gợi ý và đáp án

Nếu :checkhealth báo clipboard provider OK mà Notepad vẫn không dán được, kiểm tra xem bạn đang chạy nvim trong Windows Terminal hay trong một terminal khác chạy qua SSH/WSL. Trong hai trường hợp sau, Neovim không nói chuyện trực tiếp với Win32 clipboard, nên cần win32yank.exe trong PATH.

Quyền thực thi và ExecutionPolicy

Neovim không cần quyền admin, nhưng nó gọi shell, và shell trên Windows 11 có policy riêng. Hai triệu chứng điển hình:

  • pwsh in ra ... cannot be loaded because running scripts is disabled on this system. Đây là ExecutionPolicy ở mức Restricted. Sửa ở phạm vi người dùng, không cần admin:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
  • Plugin gọi sh -c "..." và báo sh: command not found. Đây là plugin viết cho Unix, trên Windows phải có Git for Windows trong PATH để có sh.exe, hoặc tìm option cấu hình cho plugin đó.

Nếu nvim chỉ chạy được khi gõ đường dẫn đầy đủ, PATH chưa được nạp sau khi cài. Đóng hẳn terminal cũ, mở lại, rồi kiểm tra where.exe nvim.

Thử ngay: trong config của bạn, tìm mọi chỗ gọi vim.fn.system hoặc io.popen. Với mỗi chỗ, trả lời: lệnh đó chạy được trên cmd.exe chưa, và nó có giả định sh tồn tại không?

Gợi ý và đáp án

Cách an toàn là dùng vim.system() với danh sách đối số thay vì nối chuỗi, ví dụ vim.system({ "git", "status" }). Cách này bỏ qua quoting của shell hoàn toàn, nên hết lỗi dù shell là cmd.exe hay pwsh.

Plugin cần compiler: treesitter và fzf-native

Đây là chỗ người học bỏ cuộc nhiều nhất. Một số plugin có phần C cần build tại máy:

  • nvim-treesitter cần tree-sitter-cli và một C compiler để compile parser.
  • telescope-fzf-native.nvim cần make và compiler.

Triệu chứng: :TSInstall lua báo lỗi kiểu cc1/cl.exe không tìm thấy, hoặc parser cài xong nhưng highlight vẫn tắt. Cách xử lý gọn nhất trên Windows 11:

winget install --id Microsoft.VisualStudio.2022.BuildTools

Trong installer, chọn workload Desktop development with C++ để có cl.exe. Sau đó mở terminal mới và chạy :checkhealth nvim-treesitter. Nếu bạn không muốn cài MSVC, zig là lựa chọn nhẹ hơn và cũng dùng được làm C compiler.

Một lưu ý về hiệu năng: Windows Defender quét mọi file mới trong thư mục data của Neovim, nên lần cài parser đầu tiên có thể chậm bất thường. Nếu cần, thêm exclusion cho đúng thư mục %LOCALAPPDATA%\nvim-data, đừng tắt bảo vệ toàn máy.

Thử ngay: chạy :checkhealth và ghi lại mọi dòng có nhãn ERROR. Với mỗi dòng, xác định nó thuộc nhóm nào: path, shell, compiler hay clipboard.

Gợi ý và đáp án

Nhóm path thường hiện ở mục provider (ví dụ node, python không tìm thấy). Nhóm compiler hiện ở nvim-treesitter. Nhóm clipboard hiện ở clipboard. Ghi ra giấy ba nhóm này giúp bạn không sửa mò: mỗi nhóm có một cách xử lý riêng và không liên quan đến nhau.

Những thói quen giúp bạn không mất cả buổi tối với Windows

  • Đổi một biến, chạy lại một lần. Sửa shell thì mở nvim mới và thử :!echo hi ngay, đừng sửa mười dòng rồi mới test.
  • Luôn đọc :checkhealth trước khi google. Chín trên mười lỗi Windows đã được nó chỉ đích danh.
  • Không copy config Unix nguyên bản. set termencoding, set shell=/bin/bash, vim.opt.clipboard kèm win32yank là ba dấu hiệu config đến từ máy khác.
  • Đường dẫn có dấu cách là kẻ thù mặc định. Tên user có space làm hỏng vô số lệnh nối chuỗi, nên ưu tiên vim.system() với danh sách đối số.
  • Đừng tắt Defender để build nhanh. Thêm exclusion cho thư mục data là đủ và an toàn hơn.

Chốt lại và bước tiếp theo

Windows 11 không làm Neovim khó dùng, nó chỉ làm lỗi khó đoán hơn vì thông báo nằm ở tầng shell và compiler. Bốn nhóm cần nhớ: quoting và đường dẫn có dấu cách, shell mặc định và ExecutionPolicy, encoding/line ending, clipboard và compiler. :checkhealth là điểm xuất phát cho cả bốn.

Bài sau, chúng ta đóng gói toàn bộ cấu hình này thành một bộ dùng được hằng ngày: cấu trúc thư mục cuối cùng, cách backup và đồng bộ giữa các máy, và checklist nghiệm thu trước khi mang đi làm việc thật.

Front: Trên Windows 11, Neovim mặc định dùng shell nào? Back: cmd.exe, với shellcmdflag là /s /c.

Front: Option nào khiến đường dẫn có dấu cách bị hiểu sai khi chạy lệnh shell? Back: shellxquote: nếu để dấu nháy kép, quoting lồng nhau sẽ vỡ.

Front: Cách an toàn nhất để gọi lệnh ngoài từ Lua mà không lo quoting? Back: vim.system() với danh sách đối số, không nối chuỗi.

Front: Encoding nội bộ của Neovim là gì? Back: UTF-8, luôn luôn, không phụ thuộc hệ điều hành.

Front: Option nào quyết định LF hay CRLF khi ghi file? Back: fileformats, ví dụ unix,dos để ưu tiên LF.

Front: Vì sao set termencoding=utf-8 báo lỗi trên Neovim? Back: Neovim đã bỏ option termencoding, chỉ Vim cũ còn dùng.

Front: Làm sao mở lại một file bị đọc sai bảng mã CP1258? Back: Dùng :e ++enc=cp1258 để ép đọc lại với encoding đó.

Front: Icon và emoji hiện thành ô vuông là lỗi gì? Back: Lỗi font, không phải encoding; cần terminal hỗ trợ Nerd Font.

Front: Bản Neovim chính thức cho Windows có cần win32yank không? Back: Không, clipboard tích hợp sẵn; chỉ cần vim.opt.clipboard = unnamedplus.

Front: Khi nào thực sự cần win32yank.exe? Back: Khi chạy Neovim trong WSL hoặc qua SSH tới máy khác.

Front: Lỗi PowerShell 'running scripts is disabled' do đâu? Back: ExecutionPolicy ở mức Restricted; sửa bằng Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.

Front: Plugin báo sh: command not found trên Windows, nguyên nhân? Back: Plugin giả định shell Unix; cần Git for Windows trong PATH hoặc option riêng của plugin.

Front: nvim-treesitter cần gì để compile parser trên Windows? Back: tree-sitter-cli và một C compiler như cl.exe từ MSVC Build Tools hoặc zig.

Front: telescope-fzf-native.nvim cần công cụ gì? Back: make cùng một C compiler; thiếu thì native extension không build được.

Front: Bước đầu tiên nên làm khi gặp lỗi lạ trên Windows? Back: Chạy :checkhealth và đọc các dòng ERROR trước khi tìm kiếm.

Neovim trên Windows 11 mặc định gọi _____ làm shell, với flag _____. Đường dẫn có dấu cách thường vỡ vì option _____ tạo quoting lồng nhau sai. Cách gọi lệnh ngoài an toàn nhất từ Lua là _____ với danh sách đối số. Option _____ quyết định ghi file bằng LF hay CRLF, còn _____ nói cách đọc ghi file.