diff --git a/nvim/.config/nvim/lua/romanzy/custom/mini_files.lua b/nvim/.config/nvim/lua/romanzy/custom/mini_files.lua new file mode 100644 index 0000000..16dbfd4 --- /dev/null +++ b/nvim/.config/nvim/lua/romanzy/custom/mini_files.lua @@ -0,0 +1,360 @@ +local M = {} + +local uv = vim.uv or vim.loop +local cache_ttl_ms = 500 + +local status_cache = {} +local git_root_cache = {} +local known_git_roots = {} + +local function is_path_inside(path, root) + return path == root or vim.startswith(path, root .. "/") +end + +local function new_status_node() + return { + children = {}, + status = nil, + priority = 0, + is_dir_ignored = false, + is_dir_untracked = false, + } +end + +local function is_conflict(status) + if #status < 2 then + return false + end + + local index_status = status:sub(1, 1) + local worktree_status = status:sub(2, 2) + return index_status == "U" + or worktree_status == "U" + or status == "AA" + or status == "DD" +end + +local function status_priority(status) + if #status < 2 then + return 0 + end + + if is_conflict(status) then + return 8 + end + if status == "!!" then + return 1 + end + if status == "??" then + return 2 + end + + local index_status = status:sub(1, 1) + local worktree_status = status:sub(2, 2) + + if index_status == "M" then + return 7 + end + if index_status == "D" or worktree_status == "D" then + return 5 + end + if index_status == "A" then + return 4 + end + if index_status == "R" or index_status == "C" then + return 3 + end + if worktree_status == "M" then + return 6 + end + + return 0 +end + +local function status_highlight(status) + if not status then + return nil + end + if status == "!!" then + return "Comment" + end + if is_conflict(status) then + return "MiniDiffSignDelete" + end + if status == "??" or status:sub(1, 1) == "A" then + return "MiniDiffSignAdd" + end + if status:sub(1, 1) == "D" or status:sub(2, 2) == "D" then + return "MiniDiffSignDelete" + end + if status:sub(1, 1) == "M" + or status:sub(1, 1) == "R" + or status:sub(1, 1) == "C" + or status:sub(2, 2) == "M" + then + return "MiniDiffSignChange" + end + + return nil +end + +local function insert_status(root, relative_path, status, is_directory) + local priority = status_priority(status) + if priority == 0 then + return + end + + local node = root + local segments = vim.split(relative_path, "/", { plain = true, trimempty = true }) + for index, segment in ipairs(segments) do + node.children[segment] = node.children[segment] or new_status_node() + node = node.children[segment] + + if index == #segments then + if priority > node.priority then + node.status = status + node.priority = priority + end + if is_directory and status == "!!" then + node.is_dir_ignored = true + end + if is_directory and status == "??" then + node.is_dir_untracked = true + end + end + end +end + +local function subtree_status(node) + local best_status = node.status + local best_priority = node.priority + + for _, child in pairs(node.children) do + local child_status, child_priority = subtree_status(child) + if child_priority > best_priority then + best_status = child_status + best_priority = child_priority + end + end + + return best_status, best_priority +end + +local function parse_status(output, root) + local data = { + root = root, + statuses = {}, + trie = new_status_node(), + timestamp = uv.now(), + } + + local records = vim.split(output, "\0", { plain = true, trimempty = true }) + local record_index = 1 + while record_index <= #records do + local record = records[record_index] + if #record >= 4 then + local status = record:sub(1, 2) + local relative_path = record:sub(4) + + -- With -z, rename/copy output contains the new path followed by + -- the old path. Oil highlights the new path only. + if status:sub(1, 1) == "R" or status:sub(1, 1) == "C" then + record_index = record_index + 1 + end + + local is_directory = relative_path:sub(-1) == "/" + relative_path = relative_path:gsub("^%./", ""):gsub("/$", "") + if relative_path ~= "" then + local absolute_path = vim.fs.normalize(vim.fs.joinpath(root, relative_path)) + data.statuses[absolute_path] = status + insert_status(data.trie, relative_path, status, is_directory) + end + end + record_index = record_index + 1 + end + + return data +end + +local function find_cached_root(path) + local result + for root in pairs(known_git_roots) do + if is_path_inside(path, root) and (result == nil or #root > #result) then + result = root + end + end + return result +end + +local function find_git_root(path) + local cached_root = find_cached_root(path) + if cached_root ~= nil then + return cached_root + end + + local cached = git_root_cache[path] + if cached ~= nil then + return cached or nil + end + + local result = vim.system({ "git", "-C", path, "rev-parse", "--show-toplevel" }, { text = true }):wait() + if result.code ~= 0 then + git_root_cache[path] = false + return nil + end + + local root = vim.trim(result.stdout) + if root == "" then + git_root_cache[path] = false + return nil + end + + root = vim.fs.normalize(root) + git_root_cache[path] = root + known_git_roots[root] = true + return root +end + +local function get_status_data(fs_entry) + local path = fs_entry.path + if fs_entry.fs_type ~= "directory" then + path = vim.fs.dirname(path) + end + path = vim.fs.normalize(path) + + local root = find_git_root(path) + if root == nil then + return nil + end + + local cached = status_cache[root] + if cached ~= nil and uv.now() - cached.timestamp < cache_ttl_ms then + return cached + end + + local result = vim.system({ + "git", + "-C", + root, + "--no-optional-locks", + "status", + "--porcelain", + "--ignored", + "-z", + }, { text = true }):wait() + if result.code ~= 0 then + status_cache[root] = parse_status("", root) + return status_cache[root] + end + + local data = parse_status(result.stdout, root) + status_cache[root] = data + return data +end + +local function lookup_status(data, fs_entry) + local path = vim.fs.normalize(fs_entry.path) + local direct_status = data.statuses[path] + if fs_entry.fs_type ~= "directory" and direct_status ~= nil then + return direct_status + end + + if not is_path_inside(path, data.root) then + return nil + end + + local relative_path = path:sub(#data.root + 2) + local node = data.trie + for _, segment in ipairs(vim.split(relative_path, "/", { plain = true, trimempty = true })) do + if node.is_dir_ignored then + return "!!" + end + if node.is_dir_untracked then + return "??" + end + + node = node.children[segment] + if node == nil then + return nil + end + end + + if node.is_dir_ignored then + return "!!" + end + if node.is_dir_untracked then + return "??" + end + if next(node.children) == nil then + return node.status + end + + local status = subtree_status(node) + -- Match the Oil configuration: an ignored descendant does not grey its + -- parent directory unless Git reported that directory as ignored. + if fs_entry.fs_type == "directory" and status == "!!" then + return nil + end + return status +end + +function M.filter(fs_entry) + return fs_entry.name ~= ".git" +end + +function M.highlight(fs_entry) + local data = get_status_data(fs_entry) + local status = data == nil and nil or lookup_status(data, fs_entry) + return status_highlight(status) + or (fs_entry.fs_type == "directory" and "MiniFilesDirectory" or "MiniFilesFile") +end + +function M.invalidate() + status_cache = {} + git_root_cache = {} + known_git_roots = {} +end + +local function refresh() + M.invalidate() + if _G.MiniFiles == nil or MiniFiles.get_explorer_state() == nil then + return + end + + vim.schedule(function() + if MiniFiles.get_explorer_state() ~= nil then + MiniFiles.refresh() + end + end) +end + +function M.setup() + local group = vim.api.nvim_create_augroup("romanzy_mini_files_git", { clear = true }) + + vim.api.nvim_create_autocmd({ "BufWritePost", "FocusGained", "TermClose" }, { + group = group, + callback = refresh, + }) + vim.api.nvim_create_autocmd("User", { + group = group, + pattern = { + "GitSignsUpdate", + "LazyGitClosed", + "MiniFilesActionCreate", + "MiniFilesActionDelete", + "MiniFilesActionRename", + "MiniFilesActionCopy", + "MiniFilesActionMove", + }, + callback = refresh, + }) + + return { + content = { + filter = M.filter, + highlight = M.highlight, + }, + } +end + +return M diff --git a/nvim/.config/nvim/lua/romanzy/plugins/mini.lua b/nvim/.config/nvim/lua/romanzy/plugins/mini.lua index 4a21403..6a2b583 100644 --- a/nvim/.config/nvim/lua/romanzy/plugins/mini.lua +++ b/nvim/.config/nvim/lua/romanzy/plugins/mini.lua @@ -43,7 +43,7 @@ clue.setup({ icons.setup({}) -files.setup({}) +files.setup(require("romanzy.custom.mini_files").setup()) git.setup({})