diff --git a/README.md b/README.md index e5886973aec..c1c94886e8c 100644 --- a/README.md +++ b/README.md @@ -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" }, diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index 36fdc78969e..27dc506e33a 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -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* diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 81326b52c71..5afebcb22ac 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -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) diff --git a/lua/nvim-tree/actions/find-file.lua b/lua/nvim-tree/actions/find-file.lua index e07d52af213..1f5e78a237b 100644 --- a/lua/nvim-tree/actions/find-file.lua +++ b/lua/nvim-tree/actions/find-file.lua @@ -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) + -- 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 + end - if iterate_nodes(node.nodes) ~= nil then - return i + 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 - -- mandatory to iterate i - elseif node.open then - iterate_nodes(node.nodes) end end end diff --git a/lua/nvim-tree/actions/init.lua b/lua/nvim-tree/actions/init.lua index ab6065a2fb3..7295de00506 100644 --- a/lua/nvim-tree/actions/init.lua +++ b/lua/nvim-tree/actions/init.lua @@ -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 diff --git a/lua/nvim-tree/actions/movements.lua b/lua/nvim-tree/actions/movements.lua index 49a36c7760b..cc6833059a5 100644 --- a/lua/nvim-tree/actions/movements.lua +++ b/lua/nvim-tree/actions/movements.lua @@ -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 - - -- 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] + 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) - 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 diff --git a/lua/nvim-tree/colors.lua b/lua/nvim-tree/colors.lua index 9d361952cc2..fb45561485e 100644 --- a/lua/nvim-tree/colors.lua +++ b/lua/nvim-tree/colors.lua @@ -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 diff --git a/lua/nvim-tree/core.lua b/lua/nvim-tree/core.lua index bc05743970c..7ee7eba62d1 100644 --- a/lua/nvim-tree/core.lua +++ b/lua/nvim-tree/core.lua @@ -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 diff --git a/lua/nvim-tree/explorer/explore.lua b/lua/nvim-tree/explorer/explore.lua index 5b6a294da8d..70aa93c12ad 100644 --- a/lua/nvim-tree/explorer/explore.lua +++ b/lua/nvim-tree/explorer/explore.lua @@ -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 diff --git a/lua/nvim-tree/explorer/reload.lua b/lua/nvim-tree/explorer/reload.lua index f12ff05e2fb..6291e1e4939 100644 --- a/lua/nvim-tree/explorer/reload.lua +++ b/lua/nvim-tree/explorer/reload.lua @@ -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 diff --git a/lua/nvim-tree/lib.lua b/lua/nvim-tree/lib.lua index 649a468bbc3..cf768170a61 100644 --- a/lua/nvim-tree/lib.lua +++ b/lua/nvim-tree/lib.lua @@ -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 + end - return utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())[line] + 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. diff --git a/lua/nvim-tree/live-filter.lua b/lua/nvim-tree/live-filter.lua new file mode 100644 index 00000000000..7fa8c5f089d --- /dev/null +++ b/lua/nvim-tree/live-filter.lua @@ -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", "", "stopinsert", {}) +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 diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua index 32d17a72d28..b583cda82c9 100644 --- a/lua/nvim-tree/renderer/builder.lua +++ b/lua/nvim-tree/renderer/builder.lua @@ -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 diff --git a/lua/nvim-tree/renderer/components/padding.lua b/lua/nvim-tree/renderer/components/padding.lua index 50375cc4a5c..d455859b1d7 100644 --- a/lua/nvim-tree/renderer/components/padding.lua +++ b/lua/nvim-tree/renderer/components/padding.lua @@ -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 diff --git a/lua/nvim-tree/renderer/init.lua b/lua/nvim-tree/renderer/init.lua index 040f26933e0..be6917dcc25 100644 --- a/lua/nvim-tree/renderer/init.lua +++ b/lua/nvim-tree/renderer/init.lua @@ -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() diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua index 017c5765c22..d0ddf2d281b 100644 --- a/lua/nvim-tree/utils.lua +++ b/lua/nvim-tree/utils.lua @@ -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