🌿 Tích hợp Git và terminal trong Neovim

By 3T1C

Sau module 2, bạn đã có LSP, Telescope và Treesitter chạy ổn, leader key đã thuộc. Bước tiếp theo để cấu hình "dùng được cho công việc thật" là rút ngắn vòng lặp làm việc: xem diff, stage hunk, chạy test và build ngay trong Neovim, không phải alt-tab sang cửa sổ khác. Bài này ghép hai mảnh đó lại và xử lý phần Windows 11 hay làm hỏng trải nghiệm.

Mục tiêu bài này

  • Thêm gitsigns.nvim và biết khi nào cần thêm vim-fugitive hoặc neogit.
  • Khai báo shell tường minh trên Windows 11 để :!, :make và :terminal chạy đúng.
  • Dựng terminal buffer bằng toggleterm.nvim với keybinding vào workflow đã có.
  • Nhận diện và sửa bốn lỗi hiển thị, hiệu lực phổ biến nhất.

Ví dụ mẫu: từ buffer trống đến vòng lặp Git và terminal

Mình vẫn giữ nguyên tắc tách theo domain, không nhồi vào init.lua.

%LOCALAPPDATA%\nvim\
  init.lua
  lua\
    config\
      options.lua
      keymaps.lua
    plugins\
      git.lua
      terminal.lua

Trên Windows 11, ~/.config/nvim chính là %LOCALAPPDATA%\nvim. Nếu bạn dùng đường dẫn Unix trong tài liệu trên mạng, nhớ quy đổi.

  1. Khai báo shell trong lua/config/options.lua, trước khi cài bất kỳ plugin nào.
  2. Thêm lua/plugins/git.lua với gitsigns.nvim và keybinding hunk.
  3. Thêm vim-fugitive hoặc neogit chỉ khi bạn thật sự cần.
  4. Thêm lua/plugins/terminal.lua với toggleterm.nvim.
  5. Chạy :Lazy sync, mở một file trong repo và kiểm tra vòng lặp làm việc.

Bước 1. Shell trước, plugin sau.

-- lua/config/options.lua
if vim.fn.has("win32") == 1 then
  local pwsh = vim.fn.exepath("pwsh.exe")
  vim.o.shell = pwsh ~= "" and pwsh or "powershell.exe"
  vim.o.shellcmdflag = "-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command "
  vim.o.shellquote = ""
  vim.o.shellxquote = ""
  vim.o.termguicolors = true
end

Lý do từng dòng: vim.fn.exepath trả về chuỗi rỗng khi không tìm thấy, nên bạn ưu tiên PowerShell 7 rồi mới fallback về powershell.exe có sẵn. Dấu cách ở cuối -Command là bắt buộc, thiếu nó Neovim nối liền lệnh vào flag và PowerShell hiểu sai tham số. shellquote và shellxquote để rỗng vì PowerShell tự xử lý quote theo cách riêng; giữ giá trị mặc định của Vim sẽ tạo ra lệnh lồng quote sai.

Bước 2. Lớp Git trong buffer.

-- lua/plugins/git.lua
return {
  {
    "lewis6991/gitsigns.nvim",
    event = { "BufReadPre", "BufNewFile" },
    opts = {
      signs = {
        add = { text = "+" },
        change = { text = "~" },
        delete = { text = "_" },
      },
      on_attach = function(bufnr)
        local gs = require("gitsigns")
        local map = function(lhs, rhs, desc)
          vim.keymap.set("n", lhs, rhs, { buffer = bufnr, desc = desc })
        end
        map("]h", function() gs.nav_hunk("next") end, "Hunk kế tiếp")
        map("[h", function() gs.nav_hunk("prev") end, "Hunk trước")
        map("<leader>gp", gs.preview_hunk, "Preview hunk")
        map("<leader>gs", gs.stage_hunk, "Stage hunk")
        map("<leader>gr", gs.reset_hunk, "Reset hunk")
        map("<leader>gb", function() gs.blame_line({ full = true }) end, "Blame dòng")
      end,
    },
  },
}

event = { "BufReadPre", "BufNewFile" } để sign có sẵn ngay khi mở file, không phải chờ gõ lệnh đầu tiên. Mapping đặt buffer = bufnr để chỉ ảnh hưởng buffer hiện tại, nhờ vậy ]h và [h không đè lên bất cứ thứ gì global. Nếu bản gitsigns của bạn cũ hơn, thay gs.nav_hunk("next") bằng gs.next_hunk và gs.prev_hunk.

Bước 3. Chỉ thêm Git UI khi cần.

{ "tpope/vim-fugitive", cmd = { "Git", "G" } },

cmd khiến plugin chỉ nạp khi bạn gõ :Git, nên không tốn thời gian khởi động. Chọn theo nhu cầu: cần :Git blame, :Git push dạng command thì dùng fugitive; muốn giao diện đầy đủ kiểu Magit thì dùng neogit. Đừng cài cả ba nếu bạn chỉ cần xem và stage hunk, vì mỗi lớp thêm là một chỗ nữa để lỗi.

Bước 4. Terminal tích hợp.

-- lua/plugins/terminal.lua
return {
  {
    "akinsho/toggleterm.nvim",
    version = "*",
    opts = {
      size = 12,
      open_mapping = [[<C-\>]],
      direction = "horizontal",
      shade_terminals = false,
      start_in_insert = true,
    },
  },
}

shade_terminals = false là mẹo riêng cho Windows: lớp phủ nền tối của toggleterm dựa vào truecolor, và trên một số terminal ngoài nó tạo ra dải màu lạ. Tắt đi thì an toàn hơn, bạn vẫn phân biệt được cửa sổ terminal nhờ border và statusline.

Note: Nếu bạn quen Git Bash, có thể đặt vim.o.shell = "C:\\Program Files\\Git\\bin\\bash.exe" cùng vim.o.shellcmdflag = "-c". Đổi lại, bạn phải tự lo phần quote và một số lệnh Windows sẽ không gọi được. Với công việc hằng ngày, PowerShell 7 là lựa chọn ít ma sát nhất.

Bước 5. Kiểm tra. Chạy :Lazy sync, mở một file trong repo, đi ]h vài lần, bấm <leader>gp, rồi <C-\> và gõ git status. Muốn về normal mode từ terminal buffer, nhấn <Esc> hoặc <C-\><C-n>.

Bài thực hành trên cấu hình capstone

Bài 1. Repo capstone của bạn có một file bị sửa 3 chỗ. Dùng gitsigns để stage riêng từng hunk mà không rời Neovim. Viết lại chuỗi thao tác.

Gợi ý và đáp án

Mở file, đi ]h để tới hunk đầu, bấm <leader>gp xem trước, <leader>gs để stage, lặp lại cho hai hunk còn lại. Kiểm tra bằng :Gitsigns diffthis hoặc mở terminal và chạy git diff --staged. Điểm cần nhớ: stage_hunk thao tác trên hunk tại vị trí con trỏ, không phải toàn file.

Bài 2. Viết đoạn Lua đặt shell ưu tiên pwsh.exe, fallback powershell.exe, và in ra shell đang dùng khi bạn nghi ngờ cấu hình sai.

Gợi ý và đáp án

local pwsh = vim.fn.exepath("pwsh.exe")
vim.o.shell = pwsh ~= "" and pwsh or "powershell.exe"
vim.keymap.set("n", "<leader>ush", function()
  print(vim.o.shell .. " | " .. vim.o.shellcmdflag)
end, { desc = "Xem shell hiện tại" })

Mapping này cực hữu ích khi bạn sửa options.lua nhiều lần trong ngày: in ra cả shellcmdflag để phát hiện ngay lỗi thiếu dấu cách cuối.

Bài 3. Chuyển terminal sang dạng float cao 80% màn hình, và giải thích vì sao không nên đặt direction = "vertical" khi màn hình laptop 14 inch.

Gợi ý và đáp án

opts = { open_mapping = [[<C-\>]], direction = "float", size = 12 }

Với direction = "float", size là tỉ lệ phần trăm chiều cao, nên chỉnh size = 80. Terminal dọc ăn mất chiều ngang của buffer, trong khi output test và log thường dài theo chiều ngang, nên hướng ngang hoặc float đọc dễ hơn.

Bốn lỗi hay gặp khi ghép Git và terminal

Triệu chứng Nguyên nhân Cách xử lý
:! hoặc :make báo lỗi khó hiểu, lệnh không chạy Thiếu dấu cách cuối -Command trong shellcmdflag Thêm lại dấu cách, in vim.o.shellcmdflag để kiểm tra
Terminal hiện ký tự rác, màu lệch Chưa bật termguicolors, hoặc terminal ngoài không hỗ trợ truecolor Bật termguicolors, dùng Windows Terminal
git diff báo cả file thay đổi dù chỉ sửa một dòng CRLF so với LF chưa chuẩn hóa Thống nhất .gitattributes, kiểm tra core.autocrlf
:Git báo không phải repo Đang mở file ngoài working tree, hoặc Git chưa có trong PATH Kiểm tra :pwd, chạy git --version trong terminal tích hợp

Một điểm nữa về :terminal: Windows 11 dùng ConPTY, có sẵn từ Windows 10 1809, nên bạn không cần cài winpty như các hướng dẫn cũ. Nếu thấy ai bảo cài winpty, đó là tài liệu lỗi thời.

Ôn nhanh trước khi đóng gói cấu hình

Front: gitsigns.nvim hiển thị gì trong buffer? Back: Dấu ở cột sign cho biết hunk thêm, sửa, xóa so với Git index.

Front: Ba lựa chọn công cụ Git trong Neovim và điểm khác biệt? Back: gitsigns (hunk ngay trong buffer), vim-fugitive (:Git dạng command), neogit (UI đầy đủ kiểu Magit).

Front: Vì sao phải khai báo shell tường minh trên Windows 11? Back: Để :!, :make và :terminal dùng đúng shell có pipe và UTF-8 thay vì cmd.exe mặc định.

Front: shellcmdflag đúng cho PowerShell là gì? Back: -NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command , có dấu cách ở cuối.

Front: Thiếu dấu cách cuối trong shellcmdflag gây ra chuyện gì? Back: Lệnh bị nối dính vào flag, PowerShell hiểu sai tham số, :! và :make hỏng.

Front: Trên Windows 11, :terminal chạy qua công nghệ nào? Back: ConPTY, có sẵn từ Windows 10 1809, nên không cần winpty.

Front: Làm sao thoát terminal mode về normal mode? Back: Nhấn hoặc <C->.

Front: Vì sao gitsigns nên dùng buffer-local mapping? Back: Để keybinding chỉ ảnh hưởng buffer hiện tại, không chiếm phím toàn cục.

Front: Lệnh mở terminal tích hợp ngay trong Neovim? Back: :terminal (viết tắt :term), mở một buffer terminal chạy shell đã khai báo.

Front: Muốn terminal hiện dưới dạng cửa sổ nổi và bật tắt bằng một phím thì dùng gì? Back: Plugin như toggleterm.nvim, hoặc tự viết hàm mở float window bọc :terminal.

Front: signcolumn phải ở trạng thái nào để thấy dấu của gitsigns? Back: yes hoặc auto. Nếu là no thì buffer không có cột sign và không hiện dấu nào.

Front: gitsigns không hiện dấu, hai nguyên nhân phổ biến nhất? Back: Buffer không nằm trong Git repo, hoặc Git chưa có trong PATH.

Điền khuyết: kiểm tra lại các con số và tên gọi

Trên Windows 11, Neovim dùng _____ để chạy :terminal nên không cần cài winpty. Giá trị shellcmdflag cho PowerShell phải kết thúc bằng một _____, thiếu nó thì lệnh bị nối dính vào flag. Plugin _____ hiển thị thay đổi theo từng hunk ngay trong buffer. Để thoát terminal mode về normal mode, nhấn _____ hoặc <C->. Mapping của gitsigns nên khai báo _____ để không chiếm phím ở những buffer khác.

Chốt lại: Git và terminal đã vào đúng chỗ

Bốn điểm cần mang theo khi đóng gói cấu hình:

  • Chọn một công cụ Git chính. gitsigns cho feedback tức thì trong buffer, fugitive cho command quen tay, neogit cho UI đầy đủ. Cài cả ba cũng chạy được, nhưng chỉ nên map phím cho một cái để não không phải chuyển ngữ cảnh.
  • Shell phải tường minh. Ba biến shell, shellcmdflag (và shellquote nếu dùng) là nền cho :!, :make, :terminal. Sai một dấu cách ở cuối shellcmdflag là hỏng cả chuỗi lệnh.
  • ConPTY lo phần terminal. Không cài winpty, không thêm wrapper lạ. Nếu terminal hiển thị sai ký tự, nghĩ tới encoding trước, không nghĩ tới ConPTY.
  • Mapping theo ngữ cảnh. Hành động chỉ có nghĩa trong buffer (stage hunk, reset hunk) thì khai báo buffer-local.

Cấu hình của bạn giờ đã có đường đi từ buffer tới Git và một terminal mở ngay trong editor. Bài tiếp theo đi vào phần đau nhất của cả khóa: Khắc phục lỗi đặc thù trên Windows 11, nơi chúng ta xử lý path, encoding, shell quoting và những plugin chậm bất thường, để cấu hình này không chỉ chạy được mà còn chạy ổn định mỗi ngày.