local utils = require("nvim-tree.utils") local view = require("nvim-tree.view") local core = require("nvim-tree.core") local diagnostics = require("nvim-tree.diagnostics") local FileNode = require("nvim-tree.node.file") local DirectoryNode = require("nvim-tree.node.directory") local M = {} local MAX_DEPTH = 100 ---Return the status of the node or nil if no status, depending on the type of ---status. ---@param node Node to inspect ---@param what string? type of status ---@param skip_gitignored boolean? default false ---@return boolean local function status_is_valid(node, what, skip_gitignored) if what == "git" then local git_xy = node:get_git_xy() return git_xy ~= nil and (not skip_gitignored or git_xy[1] ~= "!!") elseif what == "diag" then local diag_status = diagnostics.get_diag_status(node) return diag_status ~= nil and diag_status.value ~= nil elseif what == "opened" then return vim.fn.bufloaded(node.absolute_path) ~= 0 end return false end ---Move to the next node that has a valid status. If none found, don't move. ---@param explorer Explorer ---@param where string? where to move (forwards or backwards) ---@param what string? type of status ---@param skip_gitignored boolean? default false local function move(explorer, where, what, skip_gitignored) local first_node_line = core.get_nodes_starting_line() local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line) local iter_start, iter_end, iter_step, cur, first, nex local cursor = explorer:get_cursor_position() if cursor and cursor[1] < first_node_line then cur = cursor[1] end if where == "next" then iter_start, iter_end, iter_step = first_node_line, #nodes_by_line, 1 elseif where == "prev" then iter_start, iter_end, iter_step = #nodes_by_line, first_node_line, -1 end for line = iter_start, iter_end, iter_step do local node = nodes_by_line[line] local valid = status_is_valid(node, what, skip_gitignored) if not first and valid then first = line end if cursor and line == cursor[1] then cur = line elseif valid and cur then nex = line break end end if nex then view.set_cursor({ nex, 0 }) elseif vim.o.wrapscan and first then view.set_cursor({ first, 0 }) end end ---@param node DirectoryNode local function expand_node(node) if not node.open then -- Expand the node. -- Should never collapse since we checked open. node:expand_or_collapse(false) end end --- Move to the next node recursively. ---@param explorer Explorer ---@param what string? type of status ---@param skip_gitignored? boolean default false local function move_next_recursive(explorer, what, skip_gitignored) -- If the current node: -- * is a directory -- * and is not the root node -- * and has a git/diag status -- * and is not opened -- expand it. local node_init = explorer:get_node_at_cursor() if not node_init then return end local valid = false if node_init.name ~= ".." then -- root node cannot have a status valid = status_is_valid(node_init, what, skip_gitignored) end local node_dir = node_init:as(DirectoryNode) if node_dir and valid and not node_dir.open then node_dir:expand_or_collapse(false) end move(explorer, "next", what, skip_gitignored) local node_cur = explorer:get_node_at_cursor() if not node_cur then return end -- If we haven't moved at all at this point, return. if node_init == node_cur then return end -- i is used to limit iterations. local i = 0 local dir_cur = node_cur:as(DirectoryNode) while dir_cur and i < MAX_DEPTH do expand_node(dir_cur) move(explorer, "next", what, skip_gitignored) -- Save current node. node_cur = explorer:get_node_at_cursor() dir_cur = node_cur and node_cur:as(DirectoryNode) i = i + 1 end end --- Move to the previous node recursively. --- --- move_prev_recursive: --- --- 1) Save current as node_init. -- 2) Call a non-recursive prev. --- 3) If current node is node_init's parent, call move_prev_recursive. --- 4) Else: --- 4.1) If current node is nil, is node_init (we didn't move), or is a file, return. --- 4.2) The current file is a directory, expand it. --- 4.3) Find node_init in current window, and move to it (if not found, return). --- If node_init is the root node (name = ".."), directly move to position 1. --- 4.4) Call a non-recursive prev. --- 4.5) Save the current node and start back from 4.1. --- ---@param explorer Explorer ---@param what string? type of status ---@param skip_gitignored boolean? default false local function move_prev_recursive(explorer, what, skip_gitignored) local node_init, node_cur -- 1) node_init = explorer:get_node_at_cursor() if node_init == nil then return end -- 2) move(explorer, "prev", what, skip_gitignored) node_cur = explorer:get_node_at_cursor() if node_cur == node_init.parent then -- 3) move_prev_recursive(explorer, what, skip_gitignored) else -- i is used to limit iterations. local i = 0 while i < MAX_DEPTH do -- 4.1) if node_cur == nil or node_cur == node_init -- we didn't move or node_cur:is(FileNode) -- node is a file then return end -- 4.2) local node_dir = node_cur:as(DirectoryNode) if node_dir then expand_node(node_dir) end -- 4.3) if node_init.name == ".." then -- root node view.set_cursor({ 1, 0 }) -- move to root node (position 1) else local node_init_line = utils.find_node_line(node_init) if node_init_line < 0 then return end view.set_cursor({ node_init_line, 0 }) end -- 4.4) move(explorer, "prev", what, skip_gitignored) -- 4.5) node_cur = explorer:get_node_at_cursor() i = i + 1 end end end ---@class NavigationItemOpts ---@field where string? ---@field what string? ---@field skip_gitignored boolean? ---@field recurse boolean? ---@param opts NavigationItemOpts ---@return fun() function M.fn(opts) return function() local explorer = core.get_explorer() if not explorer then return end local recurse = false -- recurse only valid for git and diag moves. if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then recurse = opts.recurse end if not recurse then move(explorer, opts.where, opts.what, opts.skip_gitignored) return end if opts.where == "next" then move_next_recursive(explorer, opts.what, opts.skip_gitignored) elseif opts.where == "prev" then move_prev_recursive(explorer, opts.what, opts.skip_gitignored) end end end return M