-- ╭─────────────────────────────────────────────────────────╮ -- │ Setup Git Signs for Mini.Files │ -- ╰─────────────────────────────────────────────────────────╯ local nsMiniFiles = vim.api.nvim_create_namespace('mini_files_git') local autocmd = vim.api.nvim_create_autocmd local _, MiniFiles = pcall(require, 'mini.files') -- Cache for git status local gitStatusCache = {} local cacheTimeout = 2000 -- Cache timeout in milliseconds ---@type table ---@param status string ---@return string symbol, string hlGroup local function mapSymbols(status) local statusMap = { -- stylua: ignore start [" M"] = { symbol = "•", hlGroup = "GitSignsChange"}, -- Modified in the working directory ["M "] = { symbol = "✹", hlGroup = "GitSignsChange"}, -- modified in index ["MM"] = { symbol = "≠", hlGroup = "GitSignsChange"}, -- modified in both working tree and index ["A "] = { symbol = "+", hlGroup = "GitSignsAdd" }, -- Added to the staging area, new file ["AA"] = { symbol = "≈", hlGroup = "GitSignsAdd" }, -- file is added in both working tree and index ["D "] = { symbol = "-", hlGroup = "GitSignsDelete"}, -- Deleted from the staging area ["AM"] = { symbol = "⊕", hlGroup = "GitSignsChange"}, -- added in working tree, modified in index ["AD"] = { symbol = "-•", hlGroup = "GitSignsChange"}, -- Added in the index and deleted in the working directory ["R "] = { symbol = "→", hlGroup = "GitSignsChange"}, -- Renamed in the index ["U "] = { symbol = "‖", hlGroup = "GitSignsChange"}, -- Unmerged path ["UU"] = { symbol = "⇄", hlGroup = "GitSignsAdd" }, -- file is unmerged ["UA"] = { symbol = "⊕", hlGroup = "GitSignsAdd" }, -- file is unmerged and added in working tree ["??"] = { symbol = "?", hlGroup = "GitSignsDelete"}, -- Untracked files ["!!"] = { symbol = "!", hlGroup = "GitSignsChange"}, -- Ignored files -- stylua: ignore end } local result = statusMap[status] or { symbol = '?', hlGroup = 'NonText' } return result.symbol, result.hlGroup end ---@param cwd string ---@param callback function ---@return nil local function fetchGitStatus(cwd, callback) local function on_exit(content) if content.code == 0 then callback(content.stdout) vim.g.content = content.stdout end end vim.system( { 'git', 'status', '--ignored', '--porcelain' }, { text = true, cwd = cwd }, on_exit ) end ---@param str string? local function escapePattern(str) ---@diagnostic disable-next-line: need-check-nil return str:gsub('([%^%$%(%)%%%.%[%]%*%+%-%?])', '%%%1') end ---@param buf_id integer ---@param gitStatusMap table ---@return nil local function updateMiniWithGit(buf_id, gitStatusMap) vim.schedule(function() local nlines = vim.api.nvim_buf_line_count(buf_id) local cwd = vim.fs.root(buf_id, '.git') local escapedcwd = escapePattern(cwd) if vim.fn.has('win32') == 1 then escapedcwd = escapedcwd:gsub('\\', '/') end for i = 1, nlines do local entry = MiniFiles.get_fs_entry(buf_id, i) if not entry then break end local relativePath = entry.path:gsub('^' .. escapedcwd .. '/', '') local status = gitStatusMap[relativePath] if status then local symbol, hlGroup = mapSymbols(status) vim.api.nvim_buf_set_extmark(buf_id, nsMiniFiles, i - 1, 0, { sign_text = symbol, sign_hl_group = hlGroup, priority = 2, }) else end end end) end -- Thanks for the idea of gettings https://github.com/refractalize/oil-git-status.nvim signs for dirs ---@param content string ---@return table local function parseGitStatus(content) local gitStatusMap = {} -- lua match is faster than vim.split (in my experience ) for line in content:gmatch('[^\r\n]+') do local status, filePath = string.match(line, '^(..)%s+(.*)') -- Split the file path into parts local parts = {} for part in filePath:gmatch('[^/]+') do table.insert(parts, part) end -- Start with the root directory local currentKey = '' for i, part in ipairs(parts) do if i > 1 then -- Concatenate parts with a separator to create a unique key currentKey = currentKey .. '/' .. part else currentKey = part end -- If it's the last part, it's a file, so add it with its status if i == #parts then gitStatusMap[currentKey] = status else -- If it's not the last part, it's a directory. Check if it exists, if not, add it. if not gitStatusMap[currentKey] then gitStatusMap[currentKey] = status end end end end return gitStatusMap end ---@param buf_id integer ---@return nil local function updateGitStatus(buf_id) ---@diagnostic disable-next-line: param-type-mismatch if not vim.fs.root(vim.uv.cwd(), '.git') then return end local cwd = vim.fn.expand('%:p:h') local currentTime = os.time() if gitStatusCache[cwd] and currentTime - gitStatusCache[cwd].time < cacheTimeout then updateMiniWithGit(buf_id, gitStatusCache[cwd].statusMap) else fetchGitStatus(cwd, function(content) local gitStatusMap = parseGitStatus(content) gitStatusCache[cwd] = { time = currentTime, statusMap = gitStatusMap, } updateMiniWithGit(buf_id, gitStatusMap) end) end end ---@return nil local function clearCache() gitStatusCache = {} end local function augroup(name) return vim.api.nvim_create_augroup('MiniFiles_' .. name, { clear = true }) end autocmd('User', { group = augroup('start'), pattern = 'MiniFilesExplorerOpen', -- pattern = { "minifiles" }, callback = function() local bufnr = vim.api.nvim_get_current_buf() updateGitStatus(bufnr) end, }) autocmd('User', { group = augroup('close'), pattern = 'MiniFilesExplorerClose', callback = function() clearCache() end, }) autocmd('User', { group = augroup('update'), pattern = 'MiniFilesBufferUpdate', callback = function(sii) local bufnr = sii.data.buf_id local cwd = vim.fn.expand('%:p:h') if gitStatusCache[cwd] then updateMiniWithGit(bufnr, gitStatusCache[cwd].statusMap) end end, })