Skip to content

Commit 5d13cc8

Browse files
feat(#1389): api: recursive node navigation for git and diagnostics (#2525)
* feat(#1389): add next recursive for git and diag moves The recurse opt can be used to directly go to the next item showing git/diagnostic status recursively. Signed-off-by: Antonin Godard <[email protected]> * refactor: status logic in single function Rename get_status to status_is_valid. Use status_is_valid function in multiple place to avoid duplicating code. Signed-off-by: Antonin Godard <[email protected]> * feat(#1389): add prev recursive for git and diag moves Signed-off-by: Antonin Godard <[email protected]> * fix(#1389): next recursive: take root node into account The root node cannot have a status. Previously if moving from the root node, status_is_valid was trying to fetch the status from it and errored. Signed-off-by: Antonin Godard <[email protected]> * fix(#1389): doc: remove show_on_open_dirs limitation Signed-off-by: Antonin Godard <[email protected]> * feat(#1389): move find_node_line to utils Signed-off-by: Antonin Godard <[email protected]> * feat(#1389): doc: note recursive moves are to files only, tidy --------- Signed-off-by: Antonin Godard <[email protected]> Co-authored-by: Alexander Courtis <[email protected]>
1 parent 6a99f5a commit 5d13cc8

File tree

4 files changed

+260
-35
lines changed

4 files changed

+260
-35
lines changed

doc/nvim-tree-lua.txt

+24
Original file line numberDiff line numberDiff line change
@@ -1875,13 +1875,25 @@ node.open.preview_no_picker() *nvim-tree-api.node.open.preview_no_picker()*
18751875
node.navigate.git.next() *nvim-tree-api.node.navigate.git.next()*
18761876
Navigate to the next item showing git status.
18771877

1878+
*nvim-tree-api.node.navigate.git.next_recursive()*
1879+
node.navigate.git.next_recursive()
1880+
Alternative to |nvim-tree-api.node.navigate.git.next()| that navigates to
1881+
the next file showing git status, recursively.
1882+
Needs |nvim-tree.git.show_on_dirs| set.
1883+
18781884
*nvim-tree-api.node.navigate.git.next_skip_gitignored()*
18791885
node.navigate.git.next_skip_gitignored()
18801886
Same as |node.navigate.git.next()|, but skips gitignored files.
18811887

18821888
node.navigate.git.prev() *nvim-tree-api.node.navigate.git.prev()*
18831889
Navigate to the previous item showing git status.
18841890

1891+
*nvim-tree-api.node.navigate.git.prev_recursive()*
1892+
node.navigate.git.prev_recursive()
1893+
Alternative to |nvim-tree-api.node.navigate.git.prev()| that navigates to
1894+
the previous file showing git status, recursively.
1895+
Needs |nvim-tree.git.show_on_dirs| set.
1896+
18851897
*nvim-tree-api.node.navigate.git.prev_skip_gitignored()*
18861898
node.navigate.git.prev_skip_gitignored()
18871899
Same as |node.navigate.git.prev()|, but skips gitignored files.
@@ -1890,10 +1902,22 @@ node.navigate.git.prev_skip_gitignored()
18901902
node.navigate.diagnostics.next()
18911903
Navigate to the next item showing diagnostic status.
18921904

1905+
*nvim-tree-api.node.navigate.diagnostics.next_recursive()*
1906+
node.navigate.diagnostics.next_recursive()
1907+
Alternative to |nvim-tree-api.node.navigate.diagnostics.next()| that
1908+
navigates to the next file showing diagnostic status, recursively.
1909+
Needs |nvim-tree.diagnostics.show_on_dirs| set.
1910+
18931911
*nvim-tree-api.node.navigate.diagnostics.prev()*
18941912
node.navigate.diagnostics.prev()
18951913
Navigate to the next item showing diagnostic status.
18961914

1915+
*nvim-tree-api.node.navigate.diagnostics.prev_recursive()*
1916+
node.navigate.diagnostics.prev_recursive()
1917+
Alternative to |nvim-tree-api.node.navigate.diagnostics.prev()| that
1918+
navigates to the previous file showing diagnostic status, recursively.
1919+
Needs |nvim-tree.diagnostics.show_on_dirs| set.
1920+
18971921
*nvim-tree-api.node.navigate.opened.next()*
18981922
node.navigate.opened.next()
18991923
Navigate to the next |bufloaded()| item.

lua/nvim-tree/actions/moves/item.lua

+210-35
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,201 @@ local explorer_node = require "nvim-tree.explorer.node"
66
local diagnostics = require "nvim-tree.diagnostics"
77

88
local M = {}
9+
local MAX_DEPTH = 100
10+
11+
---Return the status of the node or nil if no status, depending on the type of
12+
---status.
13+
---@param node table node to inspect
14+
---@param what string type of status
15+
---@param skip_gitignored boolean default false
16+
---@return boolean
17+
local function status_is_valid(node, what, skip_gitignored)
18+
if what == "git" then
19+
local git_status = explorer_node.get_git_status(node)
20+
return git_status ~= nil and (not skip_gitignored or git_status[1] ~= "!!")
21+
elseif what == "diag" then
22+
local diag_status = diagnostics.get_diag_status(node)
23+
return diag_status ~= nil and diag_status.value ~= nil
24+
elseif what == "opened" then
25+
return vim.fn.bufloaded(node.absolute_path) ~= 0
26+
end
27+
28+
return false
29+
end
30+
31+
---Move to the next node that has a valid status. If none found, don't move.
32+
---@param where string where to move (forwards or backwards)
33+
---@param what string type of status
34+
---@param skip_gitignored boolean default false
35+
local function move(where, what, skip_gitignored)
36+
local node_cur = lib.get_node_at_cursor()
37+
local first_node_line = core.get_nodes_starting_line()
38+
local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, first_node_line)
39+
local iter_start, iter_end, iter_step, cur, first, nex
40+
41+
if where == "next" then
42+
iter_start, iter_end, iter_step = first_node_line, #nodes_by_line, 1
43+
elseif where == "prev" then
44+
iter_start, iter_end, iter_step = #nodes_by_line, first_node_line, -1
45+
end
46+
47+
for line = iter_start, iter_end, iter_step do
48+
local node = nodes_by_line[line]
49+
local valid = status_is_valid(node, what, skip_gitignored)
50+
51+
if not first and valid then
52+
first = line
53+
end
54+
55+
if node == node_cur then
56+
cur = line
57+
elseif valid and cur then
58+
nex = line
59+
break
60+
end
61+
end
62+
63+
if nex then
64+
view.set_cursor { nex, 0 }
65+
elseif vim.o.wrapscan and first then
66+
view.set_cursor { first, 0 }
67+
end
68+
end
69+
70+
local function expand_node(node)
71+
if not node.open then
72+
-- Expand the node.
73+
-- Should never collapse since we checked open.
74+
lib.expand_or_collapse(node)
75+
end
76+
end
77+
78+
--- Move to the next node recursively.
79+
---@param what string type of status
80+
---@param skip_gitignored boolean default false
81+
local function move_next_recursive(what, skip_gitignored)
82+
-- If the current node:
83+
-- * is a directory
84+
-- * and is not the root node
85+
-- * and has a git/diag status
86+
-- * and is not opened
87+
-- expand it.
88+
local node_init = lib.get_node_at_cursor()
89+
if not node_init then
90+
return
91+
end
92+
local valid = false
93+
if node_init.name ~= ".." then -- root node cannot have a status
94+
valid = status_is_valid(node_init, what, skip_gitignored)
95+
end
96+
if node_init.nodes ~= nil and valid and not node_init.open then
97+
lib.expand_or_collapse(node_init)
98+
end
99+
100+
move("next", what, skip_gitignored)
101+
102+
local node_cur = lib.get_node_at_cursor()
103+
if not node_cur then
104+
return
105+
end
106+
107+
-- If we haven't moved at all at this point, return.
108+
if node_init == node_cur then
109+
return
110+
end
111+
112+
-- i is used to limit iterations.
113+
local i = 0
114+
local is_dir = node_cur.nodes ~= nil
115+
while is_dir and i < MAX_DEPTH do
116+
expand_node(node_cur)
117+
118+
move("next", what, skip_gitignored)
119+
120+
-- Save current node.
121+
node_cur = lib.get_node_at_cursor()
122+
-- Update is_dir.
123+
if node_cur then
124+
is_dir = node_cur.nodes ~= nil
125+
else
126+
is_dir = false
127+
end
128+
129+
i = i + 1
130+
end
131+
end
132+
133+
--- Move to the previous node recursively.
134+
---
135+
--- move_prev_recursive:
136+
---
137+
--- 1) Save current as node_init.
138+
-- 2) Call a non-recursive prev.
139+
--- 3) If current node is node_init's parent, call move_prev_recursive.
140+
--- 4) Else:
141+
--- 4.1) If current node is nil, is node_init (we didn't move), or is a file, return.
142+
--- 4.2) The current file is a directory, expand it.
143+
--- 4.3) Find node_init in current window, and move to it (if not found, return).
144+
--- If node_init is the root node (name = ".."), directly move to position 1.
145+
--- 4.4) Call a non-recursive prev.
146+
--- 4.5) Save the current node and start back from 4.1.
147+
---
148+
---@param what string type of status
149+
---@param skip_gitignored boolean default false
150+
local function move_prev_recursive(what, skip_gitignored)
151+
local node_init, node_cur
152+
153+
-- 1)
154+
node_init = lib.get_node_at_cursor()
155+
if node_init == nil then
156+
return
157+
end
158+
159+
-- 2)
160+
move("prev", what, skip_gitignored)
161+
162+
node_cur = lib.get_node_at_cursor()
163+
if node_cur == node_init.parent then
164+
-- 3)
165+
move_prev_recursive(what, skip_gitignored)
166+
else
167+
-- i is used to limit iterations.
168+
local i = 0
169+
while i < MAX_DEPTH do
170+
-- 4.1)
171+
if
172+
node_cur == nil
173+
or node_cur == node_init -- we didn't move
174+
or not node_cur.nodes -- node is a file
175+
then
176+
return
177+
end
178+
179+
-- 4.2)
180+
local node_dir = node_cur
181+
expand_node(node_dir)
182+
183+
-- 4.3)
184+
if node_init.name == ".." then -- root node
185+
view.set_cursor { 1, 0 } -- move to root node (position 1)
186+
else
187+
local node_init_line = utils.find_node_line(node_init)
188+
if node_init_line < 0 then
189+
return
190+
end
191+
view.set_cursor { node_init_line, 0 }
192+
end
193+
194+
-- 4.4)
195+
move("prev", what, skip_gitignored)
196+
197+
-- 4.5)
198+
node_cur = lib.get_node_at_cursor()
199+
200+
i = i + 1
201+
end
202+
end
203+
end
9204

10205
---@class NavigationItemOpts
11206
---@field where string
@@ -15,47 +210,27 @@ local M = {}
15210
---@return fun()
16211
function M.fn(opts)
17212
return function()
18-
local node_cur = lib.get_node_at_cursor()
19-
local first_node_line = core.get_nodes_starting_line()
20-
local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, first_node_line)
21-
local iter_start, iter_end, iter_step, cur, first, nex
213+
local recurse = false
214+
local skip_gitignored = false
22215

23-
if opts.where == "next" then
24-
iter_start, iter_end, iter_step = first_node_line, #nodes_by_line, 1
25-
elseif opts.where == "prev" then
26-
iter_start, iter_end, iter_step = #nodes_by_line, first_node_line, -1
216+
-- recurse only valid for git and diag moves.
217+
if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then
218+
recurse = opts.recurse
27219
end
28220

29-
for line = iter_start, iter_end, iter_step do
30-
local node = nodes_by_line[line]
31-
local valid = false
32-
33-
if opts.what == "git" then
34-
local git_status = explorer_node.get_git_status(node)
35-
valid = git_status ~= nil and (not opts.skip_gitignored or git_status[1] ~= "!!")
36-
elseif opts.what == "diag" then
37-
local diag_status = diagnostics.get_diag_status(node)
38-
valid = diag_status ~= nil and diag_status.value ~= nil
39-
elseif opts.what == "opened" then
40-
valid = vim.fn.bufloaded(node.absolute_path) ~= 0
41-
end
42-
43-
if not first and valid then
44-
first = line
45-
end
221+
if opts.skip_gitignored ~= nil then
222+
skip_gitignored = opts.skip_gitignored
223+
end
46224

47-
if node == node_cur then
48-
cur = line
49-
elseif valid and cur then
50-
nex = line
51-
break
52-
end
225+
if not recurse then
226+
move(opts.where, opts.what, skip_gitignored)
227+
return
53228
end
54229

55-
if nex then
56-
view.set_cursor { nex, 0 }
57-
elseif vim.o.wrapscan and first then
58-
view.set_cursor { first, 0 }
230+
if opts.where == "next" then
231+
move_next_recursive(opts.what, skip_gitignored)
232+
elseif opts.where == "prev" then
233+
move_prev_recursive(opts.what, skip_gitignored)
59234
end
60235
end
61236
end

lua/nvim-tree/api.lua

+4
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,14 @@ Api.node.navigate.parent = wrap_node(actions.moves.parent.fn(false))
210210
Api.node.navigate.parent_close = wrap_node(actions.moves.parent.fn(true))
211211
Api.node.navigate.git.next = wrap_node(actions.moves.item.fn { where = "next", what = "git" })
212212
Api.node.navigate.git.next_skip_gitignored = wrap_node(actions.moves.item.fn { where = "next", what = "git", skip_gitignored = true })
213+
Api.node.navigate.git.next_recursive = wrap_node(actions.moves.item.fn { where = "next", what = "git", recurse = true })
213214
Api.node.navigate.git.prev = wrap_node(actions.moves.item.fn { where = "prev", what = "git" })
214215
Api.node.navigate.git.prev_skip_gitignored = wrap_node(actions.moves.item.fn { where = "prev", what = "git", skip_gitignored = true })
216+
Api.node.navigate.git.prev_recursive = wrap_node(actions.moves.item.fn { where = "prev", what = "git", recurse = true })
215217
Api.node.navigate.diagnostics.next = wrap_node(actions.moves.item.fn { where = "next", what = "diag" })
218+
Api.node.navigate.diagnostics.next_recursive = wrap_node(actions.moves.item.fn { where = "next", what = "diag", recurse = true })
216219
Api.node.navigate.diagnostics.prev = wrap_node(actions.moves.item.fn { where = "prev", what = "diag" })
220+
Api.node.navigate.diagnostics.prev_recursive = wrap_node(actions.moves.item.fn { where = "prev", what = "diag", recurse = true })
217221
Api.node.navigate.opened.next = wrap_node(actions.moves.item.fn { where = "next", what = "opened" })
218222
Api.node.navigate.opened.prev = wrap_node(actions.moves.item.fn { where = "prev", what = "opened" })
219223

lua/nvim-tree/utils.lua

+22
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,28 @@ function M.find_node(nodes, fn)
116116
return node, i
117117
end
118118

119+
-- Find the line number of a node.
120+
-- Return -1 is node is nil or not found.
121+
---@param node Node|nil
122+
---@return integer
123+
function M.find_node_line(node)
124+
if not node then
125+
return -1
126+
end
127+
128+
local first_node_line = require("nvim-tree.core").get_nodes_starting_line()
129+
local nodes_by_line = M.get_nodes_by_line(require("nvim-tree.core").get_explorer().nodes, first_node_line)
130+
local iter_start, iter_end = first_node_line, #nodes_by_line
131+
132+
for line = iter_start, iter_end, 1 do
133+
if nodes_by_line[line] == node then
134+
return line
135+
end
136+
end
137+
138+
return -1
139+
end
140+
119141
-- get the node in the tree state depending on the absolute path of the node
120142
-- (grouped or hidden too)
121143
---@param path string

0 commit comments

Comments
 (0)