* fix: bookmark filter include marked-directory's children * fix(perf): add path_type to filter functions * fix: replace undefined type * fix: correct Node.fs_stat type * fix: file info popup check fs_stat not nil * refactor: add stat to should_filter, Node constructor * perf: early return if bookmark is empty --------- Co-authored-by: Alexander Courtis <alex@courtis.org>
274 lines
6.9 KiB
Lua
274 lines
6.9 KiB
Lua
local renderer = require "nvim-tree.renderer"
|
|
local view = require "nvim-tree.view"
|
|
local core = require "nvim-tree.core"
|
|
local utils = require "nvim-tree.utils"
|
|
local events = require "nvim-tree.events"
|
|
local explorer_node = require "nvim-tree.explorer.node"
|
|
|
|
---@class LibOpenOpts
|
|
---@field path string|nil path
|
|
---@field current_window boolean|nil default false
|
|
---@field winid number|nil
|
|
|
|
local M = {
|
|
target_winid = nil,
|
|
}
|
|
|
|
---@return Node|nil
|
|
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 = vim.api.nvim_win_get_cursor(winnr)
|
|
local line = cursor[1]
|
|
|
|
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
|
|
|
|
---Create a sanitized partial copy of a node, populating children recursively.
|
|
---@param node Node|nil
|
|
---@return Node|nil cloned node
|
|
local function clone_node(node)
|
|
if not node then
|
|
node = core.get_explorer()
|
|
if not node then
|
|
return nil
|
|
end
|
|
end
|
|
|
|
local n = {
|
|
absolute_path = node.absolute_path,
|
|
executable = node.executable,
|
|
extension = node.extension,
|
|
git_status = node.git_status,
|
|
has_children = node.has_children,
|
|
hidden = node.hidden,
|
|
link_to = node.link_to,
|
|
name = node.name,
|
|
open = node.open,
|
|
type = node.type,
|
|
fs_stat = node.fs_stat,
|
|
}
|
|
|
|
if type(node.nodes) == "table" then
|
|
n.nodes = {}
|
|
for _, child in ipairs(node.nodes) do
|
|
table.insert(n.nodes, clone_node(child))
|
|
end
|
|
end
|
|
|
|
return n
|
|
end
|
|
|
|
---Api.tree.get_nodes
|
|
---@return Node[]|nil
|
|
function M.get_nodes()
|
|
return clone_node(core.get_explorer())
|
|
end
|
|
|
|
-- If node is grouped, return the last node in the group. Otherwise, return the given node.
|
|
---@param node Node
|
|
---@return Node
|
|
function M.get_last_group_node(node)
|
|
while node and node.group_next do
|
|
node = node.group_next
|
|
end
|
|
|
|
---@diagnostic disable-next-line: return-type-mismatch -- it can't be nil
|
|
return node
|
|
end
|
|
|
|
---Group empty folders
|
|
-- Recursively group nodes
|
|
---@param node Node
|
|
---@return Node[]
|
|
function M.group_empty_folders(node)
|
|
local is_root = not node.parent
|
|
local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1]
|
|
if M.group_empty and not is_root and child_folder_only then
|
|
node.group_next = child_folder_only
|
|
local ns = M.group_empty_folders(child_folder_only)
|
|
node.nodes = ns or {}
|
|
return ns
|
|
end
|
|
return node.nodes
|
|
end
|
|
|
|
---Ungroup empty folders
|
|
-- If a node is grouped, ungroup it: put node.group_next to the node.nodes and set node.group_next to nil
|
|
---@param node Node
|
|
function M.ungroup_empty_folders(node)
|
|
local cur = node
|
|
while cur and cur.group_next do
|
|
cur.nodes = { cur.group_next }
|
|
cur.group_next = nil
|
|
cur = cur.nodes[1]
|
|
end
|
|
end
|
|
|
|
---@param node Node
|
|
---@return Node[]
|
|
function M.get_all_nodes_in_group(node)
|
|
local next_node = utils.get_parent_of_group(node)
|
|
local nodes = {}
|
|
while next_node do
|
|
table.insert(nodes, next_node)
|
|
next_node = next_node.group_next
|
|
end
|
|
return nodes
|
|
end
|
|
|
|
-- Toggle group empty folders
|
|
---@param head_node Node
|
|
local function toggle_group_folders(head_node)
|
|
local is_grouped = head_node.group_next ~= nil
|
|
|
|
if is_grouped then
|
|
M.ungroup_empty_folders(head_node)
|
|
else
|
|
M.group_empty_folders(head_node)
|
|
end
|
|
end
|
|
|
|
---@param node Node
|
|
function M.expand_or_collapse(node, toggle_group)
|
|
toggle_group = toggle_group or false
|
|
if node.has_children then
|
|
node.has_children = false
|
|
end
|
|
|
|
if #node.nodes == 0 then
|
|
core.get_explorer():expand(node)
|
|
end
|
|
|
|
local head_node = utils.get_parent_of_group(node)
|
|
if toggle_group then
|
|
toggle_group_folders(head_node)
|
|
end
|
|
|
|
local open = M.get_last_group_node(node).open
|
|
local next_open
|
|
if toggle_group then
|
|
next_open = open
|
|
else
|
|
next_open = not open
|
|
end
|
|
for _, n in ipairs(M.get_all_nodes_in_group(head_node)) do
|
|
n.open = next_open
|
|
end
|
|
|
|
renderer.draw()
|
|
end
|
|
|
|
function M.set_target_win()
|
|
local id = vim.api.nvim_get_current_win()
|
|
local tree_id = view.get_winnr()
|
|
if tree_id and id == tree_id then
|
|
M.target_winid = 0
|
|
return
|
|
end
|
|
|
|
M.target_winid = id
|
|
end
|
|
|
|
---@param cwd string
|
|
local function handle_buf_cwd(cwd)
|
|
if M.respect_buf_cwd and cwd ~= core.get_cwd() then
|
|
require("nvim-tree.actions.root.change-dir").fn(cwd)
|
|
end
|
|
end
|
|
|
|
local function open_view_and_draw()
|
|
local cwd = vim.fn.getcwd()
|
|
view.open()
|
|
handle_buf_cwd(cwd)
|
|
renderer.draw()
|
|
end
|
|
|
|
local function should_hijack_current_buf()
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
|
local bufmodified = vim.api.nvim_buf_get_option(bufnr, "modified")
|
|
local ft = vim.api.nvim_buf_get_option(bufnr, "ft")
|
|
|
|
local should_hijack_unnamed = M.hijack_unnamed_buffer_when_opening and bufname == "" and not bufmodified and ft == ""
|
|
local should_hijack_dir = bufname ~= "" and vim.fn.isdirectory(bufname) == 1 and M.hijack_directories.enable
|
|
|
|
return should_hijack_dir or should_hijack_unnamed
|
|
end
|
|
|
|
---@param prompt_input string
|
|
---@param prompt_select string
|
|
---@param items_short string[]
|
|
---@param items_long string[]
|
|
---@param kind string|nil
|
|
---@param callback fun(item_short: string)
|
|
function M.prompt(prompt_input, prompt_select, items_short, items_long, kind, callback)
|
|
local function format_item(short)
|
|
for i, s in ipairs(items_short) do
|
|
if short == s then
|
|
return items_long[i]
|
|
end
|
|
end
|
|
return ""
|
|
end
|
|
|
|
if M.select_prompts then
|
|
vim.ui.select(items_short, { prompt = prompt_select, kind = kind, format_item = format_item }, function(item_short)
|
|
callback(item_short)
|
|
end)
|
|
else
|
|
vim.ui.input({ prompt = prompt_input, default = items_short[1] or "" }, function(item_short)
|
|
if item_short then
|
|
callback(string.lower(item_short and item_short:sub(1, 1)) or nil)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
---Open the tree, initialising as needed. Maybe hijack the current buffer.
|
|
---@param opts LibOpenOpts|nil
|
|
function M.open(opts)
|
|
opts = opts or {}
|
|
|
|
M.set_target_win()
|
|
if not core.get_explorer() or opts.path then
|
|
core.init(opts.path or vim.loop.cwd())
|
|
end
|
|
if should_hijack_current_buf() then
|
|
view.close_this_tab_only()
|
|
view.open_in_win()
|
|
renderer.draw()
|
|
elseif opts.winid then
|
|
view.open_in_win { hijack_current_buf = false, resize = false, winid = opts.winid }
|
|
renderer.draw()
|
|
elseif opts.current_window then
|
|
view.open_in_win { hijack_current_buf = false, resize = false }
|
|
renderer.draw()
|
|
else
|
|
open_view_and_draw()
|
|
end
|
|
view.restore_tab_state()
|
|
events._dispatch_on_tree_open()
|
|
end
|
|
|
|
function M.setup(opts)
|
|
M.hijack_unnamed_buffer_when_opening = opts.hijack_unnamed_buffer_when_opening
|
|
M.hijack_directories = opts.hijack_directories
|
|
M.respect_buf_cwd = opts.respect_buf_cwd
|
|
M.select_prompts = opts.select_prompts
|
|
M.group_empty = opts.renderer.group_empty
|
|
end
|
|
|
|
return M
|