feat: Highlight gitignored files (and fix g:nvim_tree_gitignore) (#268)
This commit is contained in:
@@ -56,7 +56,8 @@ let g:nvim_tree_icons = {
|
|||||||
\ 'unmerged': "",
|
\ 'unmerged': "",
|
||||||
\ 'renamed': "➜",
|
\ 'renamed': "➜",
|
||||||
\ 'untracked': "★",
|
\ 'untracked': "★",
|
||||||
\ 'deleted': ""
|
\ 'deleted': "",
|
||||||
|
\ 'ignored': "◌"
|
||||||
\ },
|
\ },
|
||||||
\ 'folder': {
|
\ 'folder': {
|
||||||
\ 'default': "",
|
\ 'default': "",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ local function get_links()
|
|||||||
FileStaged = 'NvimTreeGitStaged',
|
FileStaged = 'NvimTreeGitStaged',
|
||||||
FileDeleted = 'NvimTreeGitDeleted',
|
FileDeleted = 'NvimTreeGitDeleted',
|
||||||
Popup = 'Normal',
|
Popup = 'Normal',
|
||||||
|
GitIgnored = 'Comment',
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ function M.get_icon_state()
|
|||||||
unmerged = "",
|
unmerged = "",
|
||||||
renamed = "➜",
|
renamed = "➜",
|
||||||
untracked = "★",
|
untracked = "★",
|
||||||
deleted = ""
|
deleted = "",
|
||||||
|
ignored = "◌"
|
||||||
},
|
},
|
||||||
folder_icons = {
|
folder_icons = {
|
||||||
default = "",
|
default = "",
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
local luv = vim.loop
|
||||||
local utils = require'nvim-tree.utils'
|
local utils = require'nvim-tree.utils'
|
||||||
|
local config = require'nvim-tree.config'
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local roots = {}
|
local roots = {}
|
||||||
|
local fstat_cache = {}
|
||||||
|
|
||||||
local not_git = 'not a git repo'
|
local not_git = 'not a git repo'
|
||||||
local is_win = vim.api.nvim_call_function("has", {"win32"}) == 1
|
local is_win = vim.api.nvim_call_function("has", {"win32"}) == 1
|
||||||
@@ -30,8 +33,18 @@ local function update_root_status(root)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.get_gitexclude()
|
---Returns a list of all ignored files and directories in the given git directory.
|
||||||
return vim.fn.system("git ls-files --others --ignored --exclude-standard --directory")
|
---@param git_root string|nil
|
||||||
|
---@return table
|
||||||
|
function M.get_gitignored(git_root)
|
||||||
|
local result = vim.fn.systemlist(
|
||||||
|
"git -C '" .. (git_root or "") .. "' ls-files --others --ignored --exclude-standard --directory"
|
||||||
|
)
|
||||||
|
if result[1] and result[1]:match("^fatal:") then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.reload_roots()
|
function M.reload_roots()
|
||||||
@@ -69,10 +82,31 @@ local function create_root(cwd)
|
|||||||
end
|
end
|
||||||
|
|
||||||
update_root_status(git_root:sub(0, -2))
|
update_root_status(git_root:sub(0, -2))
|
||||||
|
M.update_gitignore_map()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.update_status(entries, cwd)
|
---Get the root of the git dir containing the given path or `nil` if it's not a
|
||||||
|
---git dir.
|
||||||
|
---@param path string
|
||||||
|
---@return string|nil
|
||||||
|
function M.git_root(path)
|
||||||
|
local git_root, git_status = get_git_root(path)
|
||||||
|
if not git_root then
|
||||||
|
if not create_root(path) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
git_root, git_status = get_git_root(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
if git_status == not_git then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return git_root
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.update_status(entries, cwd, parent_node)
|
||||||
local git_root, git_status = get_git_root(cwd)
|
local git_root, git_status = get_git_root(cwd)
|
||||||
if not git_root then
|
if not git_root then
|
||||||
if not create_root(cwd) then
|
if not create_root(cwd) then
|
||||||
@@ -87,9 +121,17 @@ function M.update_status(entries, cwd)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if not parent_node then parent_node = {} end
|
||||||
|
|
||||||
local matching_cwd = utils.path_to_matching_str( utils.path_add_trailing(git_root) )
|
local matching_cwd = utils.path_to_matching_str( utils.path_add_trailing(git_root) )
|
||||||
|
local num_ignored = 0
|
||||||
|
|
||||||
for _, node in pairs(entries) do
|
for _, node in pairs(entries) do
|
||||||
|
if parent_node.git_status == "ignored" or M.should_gitignore(node.absolute_path) then
|
||||||
|
node.git_status = "ignored"
|
||||||
|
num_ignored = num_ignored + 1
|
||||||
|
|
||||||
|
else
|
||||||
local relpath = node.absolute_path:gsub(matching_cwd, '')
|
local relpath = node.absolute_path:gsub(matching_cwd, '')
|
||||||
if node.entries ~= nil then
|
if node.entries ~= nil then
|
||||||
relpath = utils.path_add_trailing(relpath)
|
relpath = utils.path_add_trailing(relpath)
|
||||||
@@ -113,4 +155,91 @@ function M.update_status(entries, cwd)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if num_ignored > 0 and num_ignored == #entries then
|
||||||
|
parent_node.git_status = "ignored"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---A map from git roots to a list of ignored paths
|
||||||
|
local gitignore_map = {}
|
||||||
|
|
||||||
|
---Check if the given path is ignored by git.
|
||||||
|
---@param path string Absolute path
|
||||||
|
---@return boolean
|
||||||
|
function M.should_gitignore(path)
|
||||||
|
for _, paths in pairs(gitignore_map) do
|
||||||
|
if paths[path] == true then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Updates the gitignore map if it's needed. Each entry in the map is only
|
||||||
|
---updated if changes have been made to the git root's `.gitignore` or
|
||||||
|
---`.git/info/exclude` files, or it's been invalidated by the
|
||||||
|
---`invalidate_gitignore_map` function.
|
||||||
|
function M.update_gitignore_map_sync()
|
||||||
|
if not (config.get_icon_state().show_git_icon or vim.g.nvim_tree_git_hl == 1) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ignore_files = { ".gitignore", utils.path_join({".git", "info", "exclude"}) }
|
||||||
|
for git_root, git_status in pairs(roots) do
|
||||||
|
if git_status ~= not_git then
|
||||||
|
-- The mtime for `.gitignore` and `.git/info/exclude` is cached such that
|
||||||
|
-- the list of ignored files is only recreated when one of the said files
|
||||||
|
-- are modified.
|
||||||
|
for _, s in ipairs(ignore_files) do
|
||||||
|
local path = utils.path_join({git_root, s})
|
||||||
|
local stat = luv.fs_stat(path)
|
||||||
|
if stat and stat.mtime then
|
||||||
|
if not (fstat_cache[path]
|
||||||
|
and fstat_cache[path].mtime == stat.mtime.sec) then
|
||||||
|
|
||||||
|
gitignore_map[git_root] = {
|
||||||
|
_valid = false
|
||||||
|
}
|
||||||
|
fstat_cache[path] = {
|
||||||
|
mtime = stat.mtime.sec
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for git_root, paths in pairs(gitignore_map) do
|
||||||
|
if not paths._valid then
|
||||||
|
gitignore_map[git_root] = {
|
||||||
|
_valid = true
|
||||||
|
}
|
||||||
|
paths = gitignore_map[git_root]
|
||||||
|
|
||||||
|
for _, s in ipairs(M.get_gitignored(git_root)) do
|
||||||
|
if is_win then s = s:gsub("/", "\\") end
|
||||||
|
s = utils.path_remove_trailing(s)
|
||||||
|
paths[utils.path_join({git_root, s})] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Updates the gitignore map asynchronously if it's needed.
|
||||||
|
function M.update_gitignore_map()
|
||||||
|
vim.schedule(function()
|
||||||
|
M.update_gitignore_map_sync()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Force the ignore list of this path's git root to be recreated on the next
|
||||||
|
---call to `update_gitignore_map`.
|
||||||
|
---@param path string Absolute path
|
||||||
|
function M.invalidate_gitignore_map(path)
|
||||||
|
local git_root = get_git_root(path)
|
||||||
|
if git_root and gitignore_map[git_root] then
|
||||||
|
gitignore_map[git_root]._valid = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ M.Tree = {
|
|||||||
|
|
||||||
function M.init(with_open, with_render)
|
function M.init(with_open, with_render)
|
||||||
M.Tree.cwd = luv.cwd()
|
M.Tree.cwd = luv.cwd()
|
||||||
|
git.git_root(M.Tree.cwd)
|
||||||
|
git.update_gitignore_map_sync()
|
||||||
populate(M.Tree.entries, M.Tree.cwd)
|
populate(M.Tree.entries, M.Tree.cwd)
|
||||||
|
|
||||||
local stat = luv.fs_stat(M.Tree.cwd)
|
local stat = luv.fs_stat(M.Tree.cwd)
|
||||||
@@ -130,16 +132,22 @@ function M.unroll_dir(node)
|
|||||||
if #node.entries > 0 then
|
if #node.entries > 0 then
|
||||||
renderer.draw(M.Tree, true)
|
renderer.draw(M.Tree, true)
|
||||||
else
|
else
|
||||||
|
git.git_root(node.absolute_path)
|
||||||
|
git.update_gitignore_map_sync()
|
||||||
populate(node.entries, node.link_to or node.absolute_path, node)
|
populate(node.entries, node.link_to or node.absolute_path, node)
|
||||||
renderer.draw(M.Tree, true)
|
renderer.draw(M.Tree, true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function refresh_git(node)
|
local function refresh_git(node, update_gitignore)
|
||||||
git.update_status(node.entries, node.absolute_path or node.cwd)
|
if not node then node = M.Tree end
|
||||||
|
if update_gitignore == nil or update_gitignore == true then
|
||||||
|
git.update_gitignore_map_sync()
|
||||||
|
end
|
||||||
|
git.update_status(node.entries, node.absolute_path or node.cwd, node)
|
||||||
for _, entry in pairs(node.entries) do
|
for _, entry in pairs(node.entries) do
|
||||||
if entry.entries ~= nil then
|
if entry.entries and #entry.entries > 0 then
|
||||||
refresh_git(entry)
|
refresh_git(entry, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ local function link_new(cwd, name)
|
|||||||
absolute_path = absolute_path,
|
absolute_path = absolute_path,
|
||||||
link_to = link_to,
|
link_to = link_to,
|
||||||
open = open,
|
open = open,
|
||||||
|
group_next = nil, -- If node is grouped, this points to the next child dir/link node
|
||||||
entries = entries,
|
entries = entries,
|
||||||
match_name = path_to_matching_str(name),
|
match_name = path_to_matching_str(name),
|
||||||
match_path = path_to_matching_str(absolute_path),
|
match_path = path_to_matching_str(absolute_path),
|
||||||
@@ -98,36 +99,43 @@ local function should_group(cwd, dirs, files, links)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function gen_ignore_check()
|
local function gen_ignore_check(cwd)
|
||||||
|
if not cwd then cwd = luv.cwd() end
|
||||||
local ignore_list = {}
|
local ignore_list = {}
|
||||||
|
|
||||||
local function add_toignore(content)
|
|
||||||
for s in content:gmatch("[^\r\n]+") do
|
|
||||||
-- Trim trailing / from directories.
|
|
||||||
s = s:gsub("/+$", "")
|
|
||||||
ignore_list[s] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if (vim.g.nvim_tree_gitignore or 0) == 1 then
|
|
||||||
add_toignore(git.get_gitexclude())
|
|
||||||
end
|
|
||||||
|
|
||||||
if vim.g.nvim_tree_ignore and #vim.g.nvim_tree_ignore > 0 then
|
if vim.g.nvim_tree_ignore and #vim.g.nvim_tree_ignore > 0 then
|
||||||
for _, entry in pairs(vim.g.nvim_tree_ignore) do
|
for _, entry in pairs(vim.g.nvim_tree_ignore) do
|
||||||
ignore_list[entry] = true
|
ignore_list[entry] = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Check if the given path should be ignored.
|
||||||
|
---@param path string Absolute path
|
||||||
|
---@return boolean
|
||||||
return function(path)
|
return function(path)
|
||||||
local idx = path:match(".+()%.%w+$")
|
local basename = utils.path_basename(path)
|
||||||
local ignore_extension
|
|
||||||
if idx then
|
if not M.show_ignored then
|
||||||
ignore_extension = ignore_list['*'..string.sub(path, idx)]
|
if vim.g.nvim_tree_gitignore == 1 then
|
||||||
|
if git.should_gitignore(path) then return true end
|
||||||
end
|
end
|
||||||
local ignore_path = not M.show_ignored and ignore_list[path] == true
|
|
||||||
local ignore_dotfiles = not M.show_dotfiles and path:sub(1, 1) == '.'
|
local relpath = utils.path_relative(path, cwd)
|
||||||
return ignore_extension or ignore_path or ignore_dotfiles
|
if ignore_list[relpath] == true or ignore_list[basename] == true then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local idx = path:match(".+()%.%w+$")
|
||||||
|
if idx then
|
||||||
|
if ignore_list['*'..string.sub(path, idx)] == true then return true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not M.show_dotfiles then
|
||||||
|
if basename:sub(1, 1) == '.' then return true end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -160,7 +168,7 @@ function M.refresh_entries(entries, cwd, parent_node)
|
|||||||
if not name then break end
|
if not name then break end
|
||||||
num_new_entries = num_new_entries + 1
|
num_new_entries = num_new_entries + 1
|
||||||
|
|
||||||
if not should_ignore(name) then
|
if not should_ignore(utils.path_join({cwd, name})) then
|
||||||
if t == 'directory' then
|
if t == 'directory' then
|
||||||
table.insert(dirs, name)
|
table.insert(dirs, name)
|
||||||
new_entries[name] = true
|
new_entries[name] = true
|
||||||
@@ -212,6 +220,7 @@ function M.refresh_entries(entries, cwd, parent_node)
|
|||||||
if not named_entries[name] then
|
if not named_entries[name] then
|
||||||
local n = e.fn(cwd, name)
|
local n = e.fn(cwd, name)
|
||||||
if e.check(n.link_to, n.absolute_path) then
|
if e.check(n.link_to, n.absolute_path) then
|
||||||
|
git.invalidate_gitignore_map(n.absolute_path)
|
||||||
idx = 1
|
idx = 1
|
||||||
if prev then
|
if prev then
|
||||||
idx = entries_idx[prev] + 1
|
idx = entries_idx[prev] + 1
|
||||||
@@ -249,7 +258,7 @@ function M.populate(entries, cwd, parent_node)
|
|||||||
local name, t = luv.fs_scandir_next(handle)
|
local name, t = luv.fs_scandir_next(handle)
|
||||||
if not name then break end
|
if not name then break end
|
||||||
|
|
||||||
if not should_ignore(name) then
|
if not should_ignore(utils.path_join({cwd, name})) then
|
||||||
if t == 'directory' then
|
if t == 'directory' then
|
||||||
table.insert(dirs, name)
|
table.insert(dirs, name)
|
||||||
elseif t == 'file' then
|
elseif t == 'file' then
|
||||||
@@ -270,6 +279,7 @@ function M.populate(entries, cwd, parent_node)
|
|||||||
if links[1] then child_node = link_new(cwd, links[1]) end
|
if links[1] then child_node = link_new(cwd, links[1]) end
|
||||||
if luv.fs_access(child_node.absolute_path, 'R') then
|
if luv.fs_access(child_node.absolute_path, 'R') then
|
||||||
parent_node.group_next = child_node
|
parent_node.group_next = child_node
|
||||||
|
child_node.git_status = parent_node.git_status
|
||||||
M.populate(entries, child_node.absolute_path, child_node)
|
M.populate(entries, child_node.absolute_path, child_node)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -299,7 +309,7 @@ function M.populate(entries, cwd, parent_node)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
git.update_status(entries, cwd)
|
git.update_status(entries, cwd, parent_node)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ if vim.g.nvim_tree_git_hl == 1 then
|
|||||||
[" A"] = { { hl = "none" } },
|
[" A"] = { { hl = "none" } },
|
||||||
["RM"] = { { hl = "NvimTreeFileRenamed" } },
|
["RM"] = { { hl = "NvimTreeFileRenamed" } },
|
||||||
dirty = { { hl = "NvimTreeFileDirty" } },
|
dirty = { { hl = "NvimTreeFileDirty" } },
|
||||||
|
ignored = { { hl = "NvimTreeGitIgnored" } },
|
||||||
}
|
}
|
||||||
get_git_hl = function(node)
|
get_git_hl = function(node)
|
||||||
local git_status = node.git_status
|
local git_status = node.git_status
|
||||||
@@ -163,6 +164,7 @@ if icon_state.show_git_icon then
|
|||||||
[" D"] = { { icon = icon_state.icons.git_icons.deleted, hl = "NvimTreeGitDeleted" } },
|
[" D"] = { { icon = icon_state.icons.git_icons.deleted, hl = "NvimTreeGitDeleted" } },
|
||||||
["D "] = { { icon = icon_state.icons.git_icons.deleted, hl = "NvimTreeGitDeleted" } },
|
["D "] = { { icon = icon_state.icons.git_icons.deleted, hl = "NvimTreeGitDeleted" } },
|
||||||
dirty = { { icon = icon_state.icons.git_icons.unstaged, hl = "NvimTreeGitDirty" } },
|
dirty = { { icon = icon_state.icons.git_icons.unstaged, hl = "NvimTreeGitDirty" } },
|
||||||
|
ignored = { { icon = icon_state.icons.git_icons.ignored, hl = "NvimTreeGitIgnored" } },
|
||||||
}
|
}
|
||||||
|
|
||||||
get_git_icons = function(node, line, depth, icon_len)
|
get_git_icons = function(node, line, depth, icon_len)
|
||||||
|
|||||||
@@ -31,6 +31,25 @@ function M.path_split(path)
|
|||||||
return path:gmatch('[^'..path_separator..']+'..path_separator..'?')
|
return path:gmatch('[^'..path_separator..']+'..path_separator..'?')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Get the basename of the given path.
|
||||||
|
---@param path string
|
||||||
|
---@return string
|
||||||
|
function M.path_basename(path)
|
||||||
|
path = M.path_remove_trailing(path)
|
||||||
|
local i = path:match("^.*()" .. path_separator)
|
||||||
|
if not i then return path end
|
||||||
|
return path:sub(i + 1, #path)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get a path relative to another path.
|
||||||
|
---@param path string
|
||||||
|
---@param relative_to string
|
||||||
|
---@return string
|
||||||
|
function M.path_relative(path, relative_to)
|
||||||
|
local p, _ = path:gsub("^" .. M.path_to_matching_str(M.path_add_trailing(relative_to)), "")
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
function M.path_add_trailing(path)
|
function M.path_add_trailing(path)
|
||||||
if path:sub(-1) == path_separator then
|
if path:sub(-1) == path_separator then
|
||||||
return path
|
return path
|
||||||
@@ -40,7 +59,8 @@ function M.path_add_trailing(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function M.path_remove_trailing(path)
|
function M.path_remove_trailing(path)
|
||||||
return path:gsub(path_separator..'$', '')
|
local p, _ = path:gsub(path_separator..'$', '')
|
||||||
|
return p
|
||||||
end
|
end
|
||||||
|
|
||||||
M.path_separator = path_separator
|
M.path_separator = path_separator
|
||||||
|
|||||||
Reference in New Issue
Block a user