feat(live-filter): add ability to live filter out nodes in the tree (#1056)

This commit is contained in:
Kiyan 2022-05-17 10:03:49 +02:00 committed by GitHub
parent 99e32fea14
commit 6343813a35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 312 additions and 102 deletions

View File

@ -156,7 +156,7 @@ require'nvim-tree'.setup { -- BEGIN_DEFAULT_OPTS
icons = {
webdev_colors = true,
git_placement = "before",
}
},
},
hijack_directories = {
enable = true,
@ -216,6 +216,10 @@ require'nvim-tree'.setup { -- BEGIN_DEFAULT_OPTS
cmd = "trash",
require_confirm = true,
},
live_filter = {
prefix = "[FILTER]: ",
always_show_folders = true,
},
log = {
enable = false,
truncate = false,
@ -269,6 +273,7 @@ require'nvim-tree'.setup { -- BEGIN_DEFAULT_OPTS
- `S` will prompt the user to enter a path and then expands the tree to match the path
- `.` will enter vim command mode with the file the cursor is on
- `C-k` will toggle a popup with file infos about the file under the cursor
- `f` will allow you to filter nodes dynamically based on regex matching.
### Settings
@ -330,6 +335,8 @@ local list = {
{ key = "]c", action = "next_git_item" },
{ key = "-", action = "dir_up" },
{ key = "s", action = "system_open" },
{ key = "f", action = "live_filter" },
{ key = "F", action = "clear_live_filter" },
{ key = "q", action = "close" },
{ key = "g?", action = "toggle_help" },
{ key = "W", action = "collapse_all" },

View File

@ -124,7 +124,7 @@ Values may be functions. Warning: this may result in unexpected behaviour.
icons = {
webdev_colors = true,
git_placement = "before",
}
},
},
hijack_directories = {
enable = true,
@ -184,6 +184,10 @@ Values may be functions. Warning: this may result in unexpected behaviour.
cmd = "trash",
require_confirm = true,
},
live_filter = {
prefix = "[FILTER]: ",
always_show_folders = true,
},
log = {
enable = false,
truncate = false,
@ -519,6 +523,20 @@ Configuration for various actions.
'+' (system), otherwise, it will be stored in '1' and '"'.
Type: `boolean`, Default: `true`
*nvim-tree.live_filter*
Configurations for the live_filtering feature.
The live filter allows you to filter the tree nodes dynamically, based on
regex matching (see |vim.regex|).
This feature is bound to the `f` key by default.
The filter can be cleared with the `F` key by default.
*nvim-tree.live_filter.prefix*
Prefix of the filter displayed in the buffer.
Type: `string`, Default: `"[FILTER]: "`
*nvim-tree.live_filter.always_show_folders*
Wether to filter folders or not.
Type: `boolean`, Default: `true`
*nvim-tree.log*
Configuration for diagnostic logging.
@ -764,6 +782,8 @@ Defaults to:
{ key = "]c", action = "next_git_item" },
{ key = "-", action = "dir_up" },
{ key = "s", action = "system_open" },
{ key = "f", action = "live_filter" },
{ key = "F", action = "clear_live_filter" },
{ key = "q", action = "close" },
{ key = "g?", action = "toggle_help" },
{ key = 'W', action = "collapse_all" },
@ -773,6 +793,7 @@ Defaults to:
{ key = "U", action = "toggle_custom" },
}
<
The `list` option in `view.mappings.list` is a table of
- key can be either a string or a table of string (lhs)
@ -879,6 +900,11 @@ NvimTreeFileRenamed
NvimTreeFileNew
NvimTreeFileDeleted
There are 2 highlight groups for the live filter feature
NvimTreeLiveFilterPrefix
NvimTreeLiveFilterValue
==============================================================================
vinegar style *nvim-tree-vinegar*

View File

@ -438,6 +438,10 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
cmd = "trash",
require_confirm = true,
},
live_filter = {
prefix = "[FILTER]: ",
always_show_folders = true,
},
log = {
enable = false,
truncate = false,
@ -538,6 +542,7 @@ function M.setup(conf)
require("nvim-tree.view").setup(opts)
require("nvim-tree.lib").setup(opts)
require("nvim-tree.renderer").setup(opts)
require("nvim-tree.live-filter").setup(opts)
setup_vim_commands()
setup_autocommands(opts)

View File

@ -29,35 +29,37 @@ function M.fn(fname)
local function iterate_nodes(nodes)
for _, node in ipairs(nodes) do
i = i + 1
if not node.hidden then
i = i + 1
if not node.absolute_path or not uv.fs_stat(node.absolute_path) then
break
end
-- match against node absolute and link, as symlinks themselves will differ
if node.absolute_path == fname_real or node.link_to == fname_real then
return i
end
local abs_match = vim.startswith(fname_real, node.absolute_path .. utils.path_separator)
local link_match = node.link_to and vim.startswith(fname_real, node.link_to .. utils.path_separator)
local path_matches = node.nodes and (abs_match or link_match)
if path_matches then
if not node.open then
node.open = true
tree_altered = true
if not node.absolute_path or not uv.fs_stat(node.absolute_path) then
break
end
if #node.nodes == 0 then
core.get_explorer():expand(node)
end
if iterate_nodes(node.nodes) ~= nil then
-- match against node absolute and link, as symlinks themselves will differ
if node.absolute_path == fname_real or node.link_to == fname_real then
return i
end
-- mandatory to iterate i
elseif node.open then
iterate_nodes(node.nodes)
local abs_match = vim.startswith(fname_real, node.absolute_path .. utils.path_separator)
local link_match = node.link_to and vim.startswith(fname_real, node.link_to .. utils.path_separator)
local path_matches = node.nodes and (abs_match or link_match)
if path_matches then
if not node.open then
node.open = true
tree_altered = true
end
if #node.nodes == 0 then
core.get_explorer():expand(node)
end
if iterate_nodes(node.nodes) ~= nil then
return i
end
-- mandatory to iterate i
elseif node.open then
iterate_nodes(node.nodes)
end
end
end
end

View File

@ -40,6 +40,8 @@ local M = {
{ key = "]c", action = "next_git_item" },
{ key = "-", action = "dir_up" },
{ key = "s", action = "system_open" },
{ key = "f", action = "live_filter" },
{ key = "F", action = "clear_live_filter" },
{ key = "q", action = "close" },
{ key = "g?", action = "toggle_help" },
{ key = "W", action = "collapse_all" },
@ -65,6 +67,8 @@ local keypress_funcs = {
first_sibling = require("nvim-tree.actions.movements").sibling(-math.huge),
full_rename = require("nvim-tree.actions.rename-file").fn(true),
last_sibling = require("nvim-tree.actions.movements").sibling(math.huge),
live_filter = require("nvim-tree.live-filter").start_filtering,
clear_live_filter = require("nvim-tree.live-filter").clear_filter,
next_git_item = require("nvim-tree.actions.movements").find_git_item "next",
next_sibling = require("nvim-tree.actions.movements").sibling(1),
parent_node = require("nvim-tree.actions.movements").parent_node(false),
@ -92,6 +96,11 @@ function M.on_keypress(action)
if view.is_help_ui() and action ~= "toggle_help" then
return
end
if action == "live_filter" or action == "clear_live_filter" then
return keypress_funcs[action]()
end
local node = lib.get_node_at_cursor()
if not node then
return

View File

@ -6,34 +6,25 @@ local lib = require "nvim-tree.lib"
local M = {}
local function get_line_from_node(node, find_parent)
local function get_index_of(node, nodes)
local node_path = node.absolute_path
local line = 1
if find_parent then
node_path = node.absolute_path:match("(.*)" .. utils.path_separator)
end
local line = core.get_nodes_starting_line()
local function iter(nodes, recursive)
for _, _node in ipairs(nodes) do
for _, _node in ipairs(nodes) do
if not _node.hidden then
local n = lib.get_last_group_node(_node)
if node_path == n.absolute_path then
return line, _node
return line
end
line = line + 1
if _node.open == true and recursive then
local _, child = iter(_node.nodes, recursive)
if child ~= nil then
return line, child
end
end
end
end
return iter
end
function M.parent_node(should_close)
should_close = should_close or false
return function(node)
if should_close and node.open then
node.open = false
@ -64,41 +55,26 @@ function M.sibling(direction)
return
end
local iter = get_line_from_node(node, true)
local node_path = node.absolute_path
local parent = node.parent or core.get_explorer()
local parent_nodes = vim.tbl_filter(function(n)
return not n.hidden
end, parent.nodes)
local line = 0
local parent, _
local node_index = get_index_of(node, parent_nodes)
-- Check if current node is already at root nodes
for index, _node in ipairs(core.get_explorer().nodes) do
if node_path == _node.absolute_path then
line = index
end
local target_idx = node_index + direction
if target_idx < 1 then
target_idx = 1
elseif target_idx > #parent_nodes then
target_idx = #parent_nodes
end
if line > 0 then
parent = core.get_explorer()
else
_, parent = iter(core.get_explorer().nodes, true)
if parent ~= nil and #parent.nodes > 1 then
line, _ = get_line_from_node(node)(parent.nodes)
end
local target_node = parent_nodes[target_idx]
local _, line = utils.find_node(core.get_explorer().nodes, function(n)
return n.absolute_path == target_node.absolute_path
end)
-- Ignore parent line count
line = line - 1
end
local index = line + direction
if index < 1 then
index = 1
elseif index > #parent.nodes then
index = #parent.nodes
end
local target_node = parent.nodes[index]
line, _ = get_line_from_node(target_node)(core.get_explorer().nodes, true)
view.set_cursor { line, 0 }
view.set_cursor { line + 1, 0 }
end
end

View File

@ -52,6 +52,8 @@ local function get_hl_groups()
GitNew = { fg = colors.yellow },
WindowPicker = { gui = "bold", fg = "#ededed", bg = "#4493c8" },
LiveFilterPrefix = { gui = "bold", fg = colors.purple },
LiveFilterValue = { gui = "bold", fg = "#fff" },
}
end

View File

@ -1,5 +1,6 @@
local events = require "nvim-tree.events"
local explorer = require "nvim-tree.explorer"
local live_filter = require "nvim-tree.live-filter"
local view = require "nvim-tree.view"
local M = {}
@ -28,6 +29,9 @@ function M.get_nodes_starting_line()
if view.is_root_folder_visible(M.get_cwd()) then
offset = offset + 1
end
if live_filter.filter then
return offset + 1
end
return offset
end

View File

@ -6,6 +6,7 @@ local builders = require "nvim-tree.explorer.node-builders"
local common = require "nvim-tree.explorer.common"
local sorters = require "nvim-tree.explorer.sorters"
local filters = require "nvim-tree.explorer.filters"
local live_filter = require "nvim-tree.live-filter"
local M = {}
@ -76,6 +77,7 @@ function M.explore(node, status)
end
sorters.merge_sort(node.nodes, sorters.node_comparator)
live_filter.apply_filter(node)
return node.nodes
end

View File

@ -6,6 +6,7 @@ local builders = require "nvim-tree.explorer.node-builders"
local common = require "nvim-tree.explorer.common"
local filters = require "nvim-tree.explorer.filters"
local sorters = require "nvim-tree.explorer.sorters"
local live_filter = require "nvim-tree.live-filter"
local M = {}
@ -77,6 +78,7 @@ function M.reload(node, status)
end
sorters.merge_sort(node.nodes, sorters.node_comparator)
live_filter.apply_filter(node)
return node.nodes
end

View File

@ -13,23 +13,25 @@ function M.get_node_at_cursor()
if not core.get_explorer() then
return
end
local winnr = view.get_winnr()
if not winnr then
return
end
local cursor = api.nvim_win_get_cursor(view.get_winnr())
local line = cursor[1]
if view.is_help_ui() then
local help_lines = require("nvim-tree.renderer.help").compute_lines()
local help_text = utils.get_nodes_by_line(help_lines, 1)[line]
return { name = help_text }
else
if line == 1 and core.get_explorer().cwd ~= "/" and view.is_root_folder_visible() then
return { name = ".." }
end
return utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())[line]
end
if line == 1 and view.is_root_folder_visible(core.get_cwd()) then
return { name = ".." }
end
return utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())[line]
end
-- If node is grouped, return the last node in the group. Otherwise, return the given node.

View File

@ -0,0 +1,132 @@
local a = vim.api
local view = require "nvim-tree.view"
local M = {
filter = nil,
}
local function redraw()
require("nvim-tree.renderer").draw()
end
local function reset_filter(node_)
local function iterate(n)
n.hidden = false
if n.nodes then
for _, node in pairs(n.nodes) do
iterate(node)
end
end
end
iterate(node_ or TreeExplorer)
end
local overlay_bufnr = nil
local overlay_winnr = nil
local function remove_overlay()
a.nvim_win_close(overlay_winnr, { force = true })
overlay_bufnr = nil
overlay_winnr = nil
if M.filter == "" then
M.clear_filter()
end
end
local function matches(node)
local path = node.cwd or node.absolute_path
local name = vim.fn.fnamemodify(path, ":t")
return vim.regex(M.filter):match_str(name) ~= nil
end
function M.apply_filter(node_)
if not M.filter or M.filter == "" then
reset_filter(node_)
return
end
local function iterate(node)
local filtered_nodes = 0
local nodes = node.group_next and { node.group_next } or node.nodes
if nodes then
for _, n in pairs(nodes) do
iterate(n)
if n.hidden then
filtered_nodes = filtered_nodes + 1
end
end
end
local has_nodes = nodes and (M.always_show_folders or #nodes > filtered_nodes)
node.hidden = not (has_nodes or matches(node))
end
iterate(node_ or TreeExplorer)
end
local function record_char()
vim.schedule(function()
M.filter = a.nvim_buf_get_lines(overlay_bufnr, 0, -1, false)[1]
M.apply_filter()
redraw()
end)
end
local function configure_buffer_overlay()
overlay_bufnr = a.nvim_create_buf(false, true)
a.nvim_buf_attach(overlay_bufnr, true, {
on_lines = record_char,
})
a.nvim_create_autocmd("InsertLeave", {
callback = remove_overlay,
once = true,
})
a.nvim_buf_set_keymap(overlay_bufnr, "i", "<CR>", "<cmd>stopinsert<CR>", {})
end
local function create_overlay()
configure_buffer_overlay()
overlay_winnr = a.nvim_open_win(overlay_bufnr, true, {
col = 1,
row = 0,
relative = "cursor",
width = math.max(20, a.nvim_win_get_width(view.get_winnr()) - #M.prefix - 2),
height = 1,
border = "none",
style = "minimal",
})
a.nvim_buf_set_option(overlay_bufnr, "modifiable", true)
a.nvim_buf_set_lines(overlay_bufnr, 0, -1, false, { M.filter })
vim.cmd "startinsert"
a.nvim_win_set_cursor(overlay_winnr, { 1, #M.filter + 1 })
end
function M.start_filtering()
M.filter = M.filter or ""
redraw()
local row = require("nvim-tree.core").get_nodes_starting_line() - 1
local col = #M.prefix > 0 and #M.prefix - 1 or 1
view.set_cursor { row, col }
-- needs scheduling to let the cursor move before initializing the window
vim.schedule(create_overlay)
end
function M.clear_filter()
M.filter = nil
reset_filter()
redraw()
end
function M.setup(opts)
M.prefix = opts.live_filter.prefix
M.always_show_folders = opts.live_filter.always_show_folders
end
return M

View File

@ -44,6 +44,12 @@ function Builder:configure_picture_map(picture_map)
return self
end
function Builder:configure_filter(filter, prefix)
self.filter_prefix = prefix
self.filter = filter
return self
end
function Builder:configure_opened_file_highlighting(level)
if level == 1 then
self.open_file_highlight = "icon"
@ -221,8 +227,8 @@ function Builder:_build_file(node, padding, git_highlight, git_icons_tbl)
end
end
function Builder:_build_line(tree, node, idx)
local padding = pad.get_padding(self.depth, idx, tree, node, self.markers)
function Builder:_build_line(node, idx, num_children)
local padding = pad.get_padding(self.depth, idx, num_children, node, self.markers)
if self.depth > 0 then
self:_insert_highlight("NvimTreeIndentMarker", 0, string.len(padding))
@ -256,9 +262,28 @@ function Builder:_build_line(tree, node, idx)
end
end
function Builder:_get_nodes_number(nodes)
if not self.filter then
return #nodes
end
local i = 0
for _, n in pairs(nodes) do
if not n.hidden then
i = i + 1
end
end
return i
end
function Builder:build(tree)
for idx, node in ipairs(tree.nodes) do
self:_build_line(tree, node, idx)
local num_children = self:_get_nodes_number(tree.nodes)
local idx = 1
for _, node in ipairs(tree.nodes) do
if not node.hidden then
self:_build_line(node, idx, num_children)
idx = idx + 1
end
end
return self
@ -277,6 +302,15 @@ function Builder:build_header(show_header)
self.index = 1
end
if self.filter then
local filter_line = self.filter_prefix .. "/" .. self.filter .. "/"
self:_insert_line(filter_line)
local prefix_length = string.len(self.filter_prefix)
self:_insert_highlight("NvimTreeLiveFilterPrefix", 0, prefix_length)
self:_insert_highlight("NvimTreeLiveFilterValue", prefix_length, string.len(filter_line))
self.index = self.index + 1
end
return self
end

View File

@ -14,13 +14,13 @@ local function get_padding_arrows(icon_state)
end
end
local function get_padding_indent_markers(depth, idx, tree, _, markers)
local function get_padding_indent_markers(depth, idx, nodes_number, _, markers)
local padding = ""
if depth ~= 0 then
local rdepth = depth / 2
markers[rdepth] = idx ~= #tree.nodes
markers[rdepth] = idx ~= nodes_number
for i = 1, rdepth do
if idx == #tree.nodes and i == rdepth then
if idx == nodes_number and i == rdepth then
padding = padding .. M.config.indent_markers.icons.corner
elseif markers[i] then
padding = padding .. M.config.indent_markers.icons.edge

View File

@ -8,6 +8,7 @@ local icon_component = require "nvim-tree.renderer.components.icons"
local help = require "nvim-tree.renderer.help"
local git = require "nvim-tree.renderer.components.git"
local Builder = require "nvim-tree.renderer.builder"
local live_filter = require "nvim-tree.live-filter"
local api = vim.api
@ -87,6 +88,7 @@ function M.draw()
:configure_opened_file_highlighting(vim.g.nvim_tree_highlight_opened_files)
:configure_git_icons_padding(vim.g.nvim_tree_icon_padding)
:configure_git_icons_placement(M.config.icons.git_placement)
:configure_filter(live_filter.filter, live_filter.prefix)
:build_header(view.is_root_folder_visible(core.get_cwd()))
:build(core.get_explorer())
:unwrap()

View File

@ -103,23 +103,26 @@ function M.find_node(nodes, fn)
local function iter(nodes_, fn_)
local i = 1
for _, node in ipairs(nodes_) do
if fn_(node) then
return node, i
end
if node.open and #node.nodes > 0 then
local n, idx = iter(node.nodes, fn_)
i = i + idx
if n then
return n, i
if not node.hidden then
if fn_(node) then
return node, i
end
if node.open and #node.nodes > 0 then
local n, idx = iter(node.nodes, fn_)
i = i + idx
if n then
return n, i
end
else
i = i + 1
end
else
i = i + 1
end
end
return nil, i
end
local node, i = iter(nodes, fn)
i = require("nvim-tree.view").View.hide_root_folder and i - 1 or i
i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1
i = require("nvim-tree.live-filter").filter and i + 1 or i
return node, i
end
@ -132,12 +135,14 @@ function M.get_nodes_by_line(nodes_all, line_start)
local line = line_start
local function iter(nodes)
for _, node in ipairs(nodes) do
nodes_by_line[line] = node
line = line + 1
if node.open == true then
local child = iter(node.nodes)
if child ~= nil then
return child
if not node.hidden then
nodes_by_line[line] = node
line = line + 1
if node.open == true then
local child = iter(node.nodes)
if child ~= nil then
return child
end
end
end
end