This commit is contained in:
Grace Petryk 2025-10-17 05:50:37 +07:00 committed by GitHub
commit 3aec961bd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 162 additions and 66 deletions

View File

@ -23,7 +23,7 @@ local function search(search_dir, input_path)
local function iter(dir)
local realpath, path, name, stat, handle, _
local filter_status = explorer.filters:prepare()
explorer.filters:prepare()
handle, _ = vim.loop.fs_scandir(dir)
if not handle then
@ -46,7 +46,7 @@ local function search(search_dir, input_path)
break
end
if not explorer.filters:should_filter(path, stat, filter_status) then
if not explorer.filters:should_filter(path) then
if string.find(path, "/" .. input_path .. "$") then
return path
end

View File

@ -42,6 +42,7 @@ local Api = {
commands = {},
diagnostics = {},
decorator = {},
filters = {},
}
---Print error when setup not called.
@ -108,6 +109,17 @@ local function wrap_explorer_member_args(explorer_member, member_method, ...)
end)
end
---@param filter_api_method string
---@return fun(path: string): boolean
local function wrap_explorer_filter_function(filter_api_method)
return wrap(function(path)
local explorer = core.get_explorer()
if explorer then
return explorer.filters.api[filter_api_method](explorer.filters, path)
end
end)
end
---Invoke a member's method on the singleton explorer.
---Print error when setup not called.
---@param explorer_member string explorer member name
@ -367,4 +379,12 @@ end)
---@type nvim_tree.api.decorator.UserDecorator
Api.decorator.UserDecorator = UserDecorator --[[@as nvim_tree.api.decorator.UserDecorator]]
Api.filters.custom = wrap_explorer_filter_function("custom")
Api.filters.dotfile = wrap_explorer_filter_function("dotfile")
Api.filters.git_ignored = wrap_explorer_filter_function("git_ignored")
Api.filters.git_clean = wrap_explorer_filter_function("git_clean")
Api.filters.no_buffer = wrap_explorer_filter_function("no_buffer")
Api.filters.no_bookmark = wrap_explorer_filter_function("no_bookmark")
Api.filters.filter_reason = wrap_explorer_filter_function("filter_reason")
return Api

View File

@ -4,11 +4,12 @@ local M = {}
---@enum FILTER_REASON
M.FILTER_REASON = {
none = 0, -- It's not filtered
git = 1,
git_clean = 1,
buf = 2,
dotfile = 4,
custom = 8,
bookmark = 16,
git_ignore = 32
}
return M

View File

@ -4,14 +4,23 @@ local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON
local Class = require("nvim-tree.classic")
---@alias FilterType "custom" | "dotfiles" | "git_ignored" | "git_clean" | "no_buffer" | "no_bookmark"
---@alias GitFilterType "git_clean" | "git_ignored"
---@class FilterStatus
---@field project GitProject | nil
---@field bufinfo table
---@field bookmarks table
---@class (exact) Filters: Class
---@field enabled boolean
---@field state table<FilterType, boolean>
---@field api table<string, fun(self: Filters, path: string): boolean|nil>
---@field private explorer Explorer
---@field private exclude_list string[] filters.exclude
---@field private ignore_list table<string, boolean> filters.custom string table
---@field private custom_function (fun(absolute_path: string): boolean)|nil filters.custom function
---@field protected status FilterStatus
---@field private filter_cache table<string, FILTER_REASON>
local Filters = Class:extend()
---@class Filters
@ -38,6 +47,8 @@ function Filters:new(args)
no_bookmark = self.explorer.opts.filters.no_bookmark,
}
self.filter_cache = {}
local custom_filter = self.explorer.opts.filters.custom
if type(custom_filter) == "function" then
self.custom_function = custom_filter
@ -50,6 +61,35 @@ function Filters:new(args)
end
end
--- Cache filter function results so subsequent calls to the same path in a loop
--- iteration are as fast as possible
---@private
---@param fn fun(self: Filters, path: string): boolean
---@param reason FILTER_REASON
---@return fun(self: Filters, path: string): boolean
local function cache_wrapper(fn, reason)
---@param self Filters
---@param path string
---@return FILTER_REASON
---@diagnostic disable:invisible
local function inner(self, path)
if self.filter_cache[reason] == nil then
self.filter_cache[reason] = {}
end
if self.filter_cache[reason][path] == nil then
self.filter_cache[reason][path] = fn(self, path)
end
return self.filter_cache[reason][path]
end
---@diagnostic enable:invisible
return inner
end
---@private
---@param path string
---@return boolean
@ -64,10 +104,11 @@ end
---Check if the given path is git clean/ignored
---@private
---@param filter_type GitFilterType
---@param path string Absolute path
---@param project GitProject from prepare
---@return boolean
function Filters:git(path, project)
function Filters:git(filter_type, path, project)
if type(project) ~= "table" or type(project.files) ~= "table" or type(project.dirs) ~= "table" then
return false
end
@ -77,31 +118,44 @@ function Filters:git(path, project)
xy = xy or project.dirs.direct[path] and project.dirs.direct[path][1]
xy = xy or project.dirs.indirect[path] and project.dirs.indirect[path][1]
-- filter ignored; overrides clean as they are effectively dirty
if self.state.git_ignored and xy == "!!" then
if filter_type == "git_ignored" and xy == "!!" then
return true
end
-- filter clean
if self.state.git_clean and not xy then
if filter_type == "git_clean" and not xy then
return true
end
return false
end
---Check if the given path has no listed buffer
---@private
---@param path string Absolute path
---@param bufinfo table vim.fn.getbufinfo { buflisted = 1 }
---@param path string
---@return boolean
function Filters:buf(path, bufinfo)
if not self.state.no_buffer or type(bufinfo) ~= "table" then
function Filters:git_clean(path)
-- filter ignored; overrides clean as they are effectively dirty
if self.state.git_ignored and self:git_ignored("path") then
return true
else
return self:git("git_clean", path, self.status.project)
end
end
---@param path string
---@return boolean
function Filters:git_ignored(path)
return self:git("git_ignored", path, self.status.project)
end
---Check if the given path has no listed buffer
---@param path string Absolute path
---@return boolean
function Filters:buf(path)
if type(self.status.bufinfo) ~= "table" then
return false
end
-- filter files with no open buffer and directories containing no open buffers
for _, b in ipairs(bufinfo) do
for _, b in ipairs(self.status.bufinfo) do
if b.name == path or b.name:find(path .. "/", 1, true) then
return false
end
@ -110,28 +164,26 @@ function Filters:buf(path, bufinfo)
return true
end
---@private
---@param path string
---@return boolean
function Filters:dotfile(path)
return self.state.dotfiles and utils.path_basename(path):sub(1, 1) == "."
return utils.path_basename(path):sub(1, 1) == "."
end
---Bookmark is present
---@private
---@param path string
---@param path_type string|nil filetype of path
---@param bookmarks table<string, string|nil> path, filetype table of bookmarked files
---@return boolean
function Filters:bookmark(path, path_type, bookmarks)
if not self.state.no_bookmark then
return false
end
function Filters:bookmark(path)
local bookmarks = self.status.bookmarks
-- if bookmark is empty, we should see a empty filetree
if next(bookmarks) == nil then
return true
end
local stat, _ = vim.loop.fs_stat(path)
local path_type = stat and stat.type
local mark_parent = utils.path_add_trailing(path)
for mark, mark_type in pairs(bookmarks) do
if path == mark then
@ -156,21 +208,16 @@ function Filters:bookmark(path, path_type, bookmarks)
return true
end
---@private
---@param path string
---@return boolean
function Filters:custom(path)
if not self.state.custom then
return false
-- filter user's custom function
if type(self.custom_function) == "function" then
return self.custom_function(path)
end
local basename = utils.path_basename(path)
-- filter user's custom function
if self.custom_function and self.custom_function(path) then
return true
end
-- filter custom regexes
local relpath = utils.path_relative(path, vim.loop.cwd())
for pat, _ in pairs(self.ignore_list) do
@ -196,32 +243,30 @@ end
--- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 }
--- bookmarks: absolute paths to boolean
function Filters:prepare(project)
local status = {
self.status = {
project = project or {},
bufinfo = {},
bookmarks = {},
}
if self.state.no_buffer then
status.bufinfo = vim.fn.getbufinfo({ buflisted = 1 })
end
self.filter_cache = {}
self.status.bufinfo = vim.fn.getbufinfo({ buflisted = 1 })
local explorer = require("nvim-tree.core").get_explorer()
if explorer then
for _, node in pairs(explorer.marks:list()) do
status.bookmarks[node.absolute_path] = node.type
self.status.bookmarks[node.absolute_path] = node.type
end
end
return status
return self.status
end
---Check if the given path should be filtered.
---@param path string Absolute path
---@param fs_stat uv.fs_stat.result|nil fs_stat of file
---@param status table from prepare
---@return boolean
function Filters:should_filter(path, fs_stat, status)
function Filters:should_filter(path)
if not self.enabled then
return false
end
@ -231,19 +276,18 @@ function Filters:should_filter(path, fs_stat, status)
return false
end
return self:git(path, status.project)
or self:buf(path, status.bufinfo)
or self:dotfile(path)
or self:custom(path)
or self:bookmark(path, fs_stat and fs_stat.type, status.bookmarks)
return (self.state.custom and self:custom(path))
or (self.state.git_clean and self:git_clean(path))
or (self.state.git_ignored and self:git_ignored(path))
or (self.state.no_buffer and self:buf(path))
or (self.state.dotfiles and self:dotfile(path))
or (self.state.no_bookmark and self:bookmark(path))
end
--- Check if the given path should be filtered, and provide the reason why it was
---@param path string Absolute path
---@param fs_stat uv.fs_stat.result|nil fs_stat of file
---@param status table from prepare
---@return FILTER_REASON
function Filters:should_filter_as_reason(path, fs_stat, status)
function Filters:should_filter_as_reason(path)
if not self.enabled then
return FILTER_REASON.none
end
@ -252,16 +296,28 @@ function Filters:should_filter_as_reason(path, fs_stat, status)
return FILTER_REASON.none
end
if self:git(path, status.project) then
return FILTER_REASON.git
elseif self:buf(path, status.bufinfo) then
return FILTER_REASON.buf
elseif self:dotfile(path) then
return FILTER_REASON.dotfile
elseif self:custom(path) then
if not self:should_filter(path) then
return FILTER_REASON.none
end
if self.state.custom and self:custom(path) then
return FILTER_REASON.custom
elseif self:bookmark(path, fs_stat and fs_stat.type, status.bookmarks) then
elseif self.state.git_clean and self:git_clean(path) then
return FILTER_REASON.git_clean
elseif self.state.git_ignored and self:git_ignored(path) then
return FILTER_REASON.git_ignore
elseif self.state.no_buffer and self:buf(path) then
return FILTER_REASON.buf
elseif self.state.dotfiles and self:dotfile(path) then
return FILTER_REASON.dotfile
elseif self.state.no_bookmark and self:bookmark(path) then
return FILTER_REASON.bookmark
else
return FILTER_REASON.none
end
@ -284,4 +340,22 @@ function Filters:toggle(type)
end
end
---@diagnostic disable:inject-field
Filters.custom = cache_wrapper(Filters.custom, FILTER_REASON.custom)
Filters.dotfile = cache_wrapper(Filters.dotfile, FILTER_REASON.dotfile)
Filters.git_ignored = cache_wrapper(Filters.git_ignored, FILTER_REASON.git_ignore)
Filters.git_clean = cache_wrapper(Filters.git_clean, FILTER_REASON.git_clean)
Filters.buf = cache_wrapper(Filters.buf, FILTER_REASON.buf)
Filters.bookmark = cache_wrapper(Filters.bookmark, FILTER_REASON.bookmark)
---@diagnostic enable:inject-field
Filters.api = {
custom = Filters.custom,
dotfile = Filters.dotfile,
git_ignored = Filters.git_ignored,
git_clean = Filters.git_clean,
no_buffer = Filters.buf,
no_bookmark = Filters.bookmark,
}
return Filters

View File

@ -205,7 +205,7 @@ function Explorer:reload(node, project)
local profile = log.profile_start("reload %s", node.absolute_path)
local filter_status = self.filters:prepare(project)
self.filters:prepare(project)
if node.group_next then
node.nodes = { node.group_next }
@ -220,11 +220,12 @@ function Explorer:reload(node, project)
-- To reset we must 'zero' everything that we use
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
git = 0,
buf = 0,
dotfile = 0,
custom = 0,
bookmark = 0,
git_clean = 0,
git_ignore = 0,
buf = 0,
dotfile = 0,
custom = 0,
bookmark = 0,
})
while true do
@ -238,7 +239,7 @@ function Explorer:reload(node, project)
-- path incorrectly specified as an integer
local stat = vim.loop.fs_lstat(abs) ---@diagnostic disable-line param-type-mismatch
local filter_reason = self.filters:should_filter_as_reason(abs, stat, filter_status)
local filter_reason = self.filters:should_filter_as_reason(abs)
if filter_reason == FILTER_REASON.none then
remain_childs[abs] = true
@ -373,7 +374,7 @@ function Explorer:populate_children(handle, cwd, node, project, parent)
local node_ignored = node:is_git_ignored()
local nodes_by_path = utils.bool_record(node.nodes, "absolute_path")
local filter_status = parent.filters:prepare(project)
parent.filters:prepare(project)
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
git = 0,
@ -397,7 +398,7 @@ function Explorer:populate_children(handle, cwd, node, project, parent)
-- path incorrectly specified as an integer
local stat = vim.loop.fs_lstat(abs) ---@diagnostic disable-line param-type-mismatch
local filter_reason = parent.filters:should_filter_as_reason(abs, stat, filter_status)
local filter_reason = parent.filters:should_filter_as_reason(abs)
if filter_reason == FILTER_REASON.none and not nodes_by_path[abs] then
local child = node_factory.create({
explorer = self,