Skip to content

Commit 0ef3d46

Browse files
authored
feat(#1974): experimental.git.async see #2104 (#2094)
* async git watcher reload; callback hell for now * async git watcher reload; revert unnecessary extractions * async git watcher reload; callback and non-callback functions are required for sync codepaths that loop * async git watcher reload * async git watcher reload * feat(#1974): experimental.git.async * feat(#1974): experimental.git.async
1 parent 7ad1c20 commit 0ef3d46

File tree

6 files changed

+155
-55
lines changed

6 files changed

+155
-55
lines changed

doc/nvim-tree-lua.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ applying configuration.
438438
trash = true,
439439
},
440440
},
441+
experimental = {
442+
git = {
443+
async = false,
444+
},
445+
},
441446
log = {
442447
enable = false,
443448
truncate = false,
@@ -1224,6 +1229,16 @@ General UI configuration.
12241229
Prompt before trashing.
12251230
Type: `boolean`, Default: `true`
12261231

1232+
*nvim-tree.experimental*
1233+
Experimental features that may become default or optional functionality.
1234+
1235+
*nvim-tree.experimental.git.async*
1236+
Direct file writes and `.git/` writes are executed asynchronously: the
1237+
git process runs in the background. The tree updates on completion.
1238+
Other git actions such as first tree draw and explicit refreshes are still
1239+
done in the foreground.
1240+
Type: `boolean`, Default: `false`
1241+
12271242
*nvim-tree.log*
12281243
Configuration for diagnostic logging.
12291244

lua/nvim-tree.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,11 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
636636
trash = true,
637637
},
638638
},
639+
experimental = {
640+
git = {
641+
async = false,
642+
},
643+
},
639644
log = {
640645
enable = false,
641646
truncate = false,
@@ -759,6 +764,7 @@ function M.setup(conf)
759764
require("nvim-tree.diagnostics").setup(opts)
760765
require("nvim-tree.explorer").setup(opts)
761766
require("nvim-tree.git").setup(opts)
767+
require("nvim-tree.git.runner").setup(opts)
762768
require("nvim-tree.view").setup(opts)
763769
require("nvim-tree.lib").setup(opts)
764770
require("nvim-tree.renderer").setup(opts)

lua/nvim-tree/explorer/reload.lua

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,18 @@ local function update_status(nodes_by_path, node_ignored, status)
2121
end
2222
end
2323

24-
local function reload_and_get_git_project(path)
24+
-- TODO always use callback once async/await is available
25+
local function reload_and_get_git_project(path, callback)
2526
local project_root = git.get_project_root(path)
26-
git.reload_project(project_root, path)
27-
return project_root, git.get_project(project_root) or {}
27+
28+
if callback then
29+
git.reload_project(project_root, path, function()
30+
callback(project_root, git.get_project(project_root) or {})
31+
end)
32+
else
33+
git.reload_project(project_root, path)
34+
return project_root, git.get_project(project_root) or {}
35+
end
2836
end
2937

3038
local function update_parent_statuses(node, project, root)
@@ -142,18 +150,32 @@ end
142150

143151
---Refresh contents and git status for a single node
144152
---@param node table
145-
function M.refresh_node(node)
153+
function M.refresh_node(node, callback)
146154
if type(node) ~= "table" then
155+
if callback then
156+
callback()
157+
end
147158
return
148159
end
149160

150161
local parent_node = utils.get_parent_of_group(node)
151162

152-
local project_root, project = reload_and_get_git_project(node.absolute_path)
163+
if callback then
164+
reload_and_get_git_project(node.absolute_path, function(project_root, project)
165+
require("nvim-tree.explorer.reload").reload(parent_node, project)
153166

154-
require("nvim-tree.explorer.reload").reload(parent_node, project)
167+
update_parent_statuses(parent_node, project, project_root)
155168

156-
update_parent_statuses(parent_node, project, project_root)
169+
callback()
170+
end)
171+
else
172+
-- TODO use callback once async/await is available
173+
local project_root, project = reload_and_get_git_project(node.absolute_path)
174+
175+
require("nvim-tree.explorer.reload").reload(parent_node, project)
176+
177+
update_parent_statuses(parent_node, project, project_root)
178+
end
157179
end
158180

159181
---Refresh contents and git status for all nodes to a path: actual directory and links

lua/nvim-tree/explorer/watch.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ function M.create_watcher(node)
5959
else
6060
log.line("watcher", "node event executing refresh '%s'", node.absolute_path)
6161
end
62-
require("nvim-tree.explorer.reload").refresh_node(node)
63-
require("nvim-tree.renderer").draw()
62+
require("nvim-tree.explorer.reload").refresh_node(node, function()
63+
require("nvim-tree.renderer").draw()
64+
end)
6465
end)
6566
end
6667

lua/nvim-tree/git/init.lua

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ local WATCHED_FILES = {
2222
"index", -- staging area
2323
}
2424

25+
local function reload_git_status(project_root, path, project, git_status)
26+
if path then
27+
for p in pairs(project.files) do
28+
if p:find(path, 1, true) == 1 then
29+
project.files[p] = nil
30+
end
31+
end
32+
project.files = vim.tbl_deep_extend("force", project.files, git_status)
33+
else
34+
project.files = git_status
35+
end
36+
37+
project.dirs = git_utils.file_status_to_dir_status(project.files, project_root)
38+
end
39+
2540
function M.reload()
2641
if not M.config.git.enable then
2742
return {}
@@ -34,36 +49,40 @@ function M.reload()
3449
return M.projects
3550
end
3651

37-
function M.reload_project(project_root, path)
52+
function M.reload_project(project_root, path, callback)
3853
local project = M.projects[project_root]
3954
if not project or not M.config.git.enable then
55+
if callback then
56+
callback()
57+
end
4058
return
4159
end
4260

4361
if path and path:find(project_root, 1, true) ~= 1 then
62+
if callback then
63+
callback()
64+
end
4465
return
4566
end
4667

47-
local git_status = Runner.run {
68+
local opts = {
4869
project_root = project_root,
4970
path = path,
5071
list_untracked = git_utils.should_show_untracked(project_root),
5172
list_ignored = true,
5273
timeout = M.config.git.timeout,
5374
}
5475

55-
if path then
56-
for p in pairs(project.files) do
57-
if p:find(path, 1, true) == 1 then
58-
project.files[p] = nil
59-
end
60-
end
61-
project.files = vim.tbl_deep_extend("force", project.files, git_status)
76+
if callback then
77+
Runner.run(opts, function(git_status)
78+
reload_git_status(project_root, path, project, git_status)
79+
callback()
80+
end)
6281
else
63-
project.files = git_status
82+
-- TODO use callback once async/await is available
83+
local git_status = Runner.run(opts)
84+
reload_git_status(project_root, path, project, git_status)
6485
end
65-
66-
project.dirs = git_utils.file_status_to_dir_status(project.files, project_root)
6786
end
6887

6988
function M.get_project(project_root)
@@ -103,21 +122,22 @@ local function reload_tree_at(project_root)
103122
return
104123
end
105124

106-
M.reload_project(project_root)
107-
local git_status = M.get_project(project_root)
125+
M.reload_project(project_root, nil, function()
126+
local git_status = M.get_project(project_root)
108127

109-
Iterator.builder(root_node.nodes)
110-
:hidden()
111-
:applier(function(node)
112-
local parent_ignored = explorer_node.is_git_ignored(node.parent)
113-
explorer_node.update_git_status(node, parent_ignored, git_status)
114-
end)
115-
:recursor(function(node)
116-
return node.nodes and #node.nodes > 0 and node.nodes
117-
end)
118-
:iterate()
128+
Iterator.builder(root_node.nodes)
129+
:hidden()
130+
:applier(function(node)
131+
local parent_ignored = explorer_node.is_git_ignored(node.parent)
132+
explorer_node.update_git_status(node, parent_ignored, git_status)
133+
end)
134+
:recursor(function(node)
135+
return node.nodes and #node.nodes > 0 and node.nodes
136+
end)
137+
:iterate()
119138

120-
require("nvim-tree.renderer").draw()
139+
require("nvim-tree.renderer").draw()
140+
end)
121141
end
122142

123143
function M.load_project_status(cwd)

lua/nvim-tree/git/runner.lua

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function Runner:_log_raw_output(output)
6969
end
7070
end
7171

72-
function Runner:_run_git_job()
72+
function Runner:_run_git_job(callback)
7373
local handle, pid
7474
local stdout = vim.loop.new_pipe(false)
7575
local stderr = vim.loop.new_pipe(false)
@@ -78,6 +78,9 @@ function Runner:_run_git_job()
7878
local function on_finish(rc)
7979
self.rc = rc or 0
8080
if timer:is_closing() or stdout:is_closing() or stderr:is_closing() or (handle and handle:is_closing()) then
81+
if callback then
82+
callback()
83+
end
8184
return
8285
end
8386
timer:stop()
@@ -91,6 +94,10 @@ function Runner:_run_git_job()
9194
end
9295

9396
pcall(vim.loop.kill, pid)
97+
98+
if callback then
99+
callback()
100+
end
94101
end
95102

96103
local opts = self:_getopts(stdout, stderr)
@@ -142,25 +149,7 @@ function Runner:_wait()
142149
end
143150
end
144151

145-
-- This module runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
146-
function Runner.run(opts)
147-
local profile = log.profile_start("git job %s %s", opts.project_root, opts.path)
148-
149-
local self = setmetatable({
150-
project_root = opts.project_root,
151-
path = opts.path,
152-
list_untracked = opts.list_untracked,
153-
list_ignored = opts.list_ignored,
154-
timeout = opts.timeout or 400,
155-
output = {},
156-
rc = nil, -- -1 indicates timeout
157-
}, Runner)
158-
159-
self:_run_git_job()
160-
self:_wait()
161-
162-
log.profile_end(profile)
163-
152+
function Runner:_finalise(opts)
164153
if self.rc == -1 then
165154
log.line("git", "job timed out %s %s", opts.project_root, opts.path)
166155
timeouts = timeouts + 1
@@ -179,8 +168,55 @@ function Runner.run(opts)
179168
else
180169
log.line("git", "job success %s %s", opts.project_root, opts.path)
181170
end
171+
end
172+
173+
--- Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
174+
--- @param opts table
175+
--- @param callback function|nil executed passing return when complete
176+
--- @return table|nil status by absolute path, nil if callback present
177+
function Runner.run(opts, callback)
178+
local self = setmetatable({
179+
project_root = opts.project_root,
180+
path = opts.path,
181+
list_untracked = opts.list_untracked,
182+
list_ignored = opts.list_ignored,
183+
timeout = opts.timeout or 400,
184+
output = {},
185+
rc = nil, -- -1 indicates timeout
186+
}, Runner)
187+
188+
local async = callback ~= nil and self.config.git_async
189+
local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", opts.project_root, opts.path)
190+
191+
if async and callback then
192+
-- async, always call back
193+
self:_run_git_job(function()
194+
log.profile_end(profile)
195+
196+
self:_finalise(opts)
197+
198+
callback(self.output)
199+
end)
200+
else
201+
-- sync, maybe call back
202+
self:_run_git_job()
203+
self:_wait()
204+
205+
log.profile_end(profile)
206+
207+
self:_finalise(opts)
208+
209+
if callback then
210+
callback(self.output)
211+
else
212+
return self.output
213+
end
214+
end
215+
end
182216

183-
return self.output
217+
function Runner.setup(opts)
218+
Runner.config = {}
219+
Runner.config.git_async = opts.experimental.git.async
184220
end
185221

186222
return Runner

0 commit comments

Comments
 (0)