* 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
This commit is contained in:
Alexander Courtis 2023-04-03 16:20:52 +10:00 committed by GitHub
parent 7ad1c204c4
commit 0ef3d4613f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 173 additions and 73 deletions

View File

@ -438,6 +438,11 @@ applying configuration.
trash = true, trash = true,
}, },
}, },
experimental = {
git = {
async = false,
},
},
log = { log = {
enable = false, enable = false,
truncate = false, truncate = false,
@ -1224,6 +1229,16 @@ General UI configuration.
Prompt before trashing. Prompt before trashing.
Type: `boolean`, Default: `true` Type: `boolean`, Default: `true`
*nvim-tree.experimental*
Experimental features that may become default or optional functionality.
*nvim-tree.experimental.git.async*
Direct file writes and `.git/` writes are executed asynchronously: the
git process runs in the background. The tree updates on completion.
Other git actions such as first tree draw and explicit refreshes are still
done in the foreground.
Type: `boolean`, Default: `false`
*nvim-tree.log* *nvim-tree.log*
Configuration for diagnostic logging. Configuration for diagnostic logging.

View File

@ -636,6 +636,11 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
trash = true, trash = true,
}, },
}, },
experimental = {
git = {
async = false,
},
},
log = { log = {
enable = false, enable = false,
truncate = false, truncate = false,
@ -759,6 +764,7 @@ function M.setup(conf)
require("nvim-tree.diagnostics").setup(opts) require("nvim-tree.diagnostics").setup(opts)
require("nvim-tree.explorer").setup(opts) require("nvim-tree.explorer").setup(opts)
require("nvim-tree.git").setup(opts) require("nvim-tree.git").setup(opts)
require("nvim-tree.git.runner").setup(opts)
require("nvim-tree.view").setup(opts) require("nvim-tree.view").setup(opts)
require("nvim-tree.lib").setup(opts) require("nvim-tree.lib").setup(opts)
require("nvim-tree.renderer").setup(opts) require("nvim-tree.renderer").setup(opts)

View File

@ -21,10 +21,18 @@ local function update_status(nodes_by_path, node_ignored, status)
end end
end end
local function reload_and_get_git_project(path) -- TODO always use callback once async/await is available
local function reload_and_get_git_project(path, callback)
local project_root = git.get_project_root(path) local project_root = git.get_project_root(path)
git.reload_project(project_root, path)
return project_root, git.get_project(project_root) or {} if callback then
git.reload_project(project_root, path, function()
callback(project_root, git.get_project(project_root) or {})
end)
else
git.reload_project(project_root, path)
return project_root, git.get_project(project_root) or {}
end
end end
local function update_parent_statuses(node, project, root) local function update_parent_statuses(node, project, root)
@ -142,18 +150,32 @@ end
---Refresh contents and git status for a single node ---Refresh contents and git status for a single node
---@param node table ---@param node table
function M.refresh_node(node) function M.refresh_node(node, callback)
if type(node) ~= "table" then if type(node) ~= "table" then
if callback then
callback()
end
return return
end end
local parent_node = utils.get_parent_of_group(node) local parent_node = utils.get_parent_of_group(node)
local project_root, project = reload_and_get_git_project(node.absolute_path) if callback then
reload_and_get_git_project(node.absolute_path, function(project_root, project)
require("nvim-tree.explorer.reload").reload(parent_node, project)
require("nvim-tree.explorer.reload").reload(parent_node, project) update_parent_statuses(parent_node, project, project_root)
update_parent_statuses(parent_node, project, project_root) callback()
end)
else
-- TODO use callback once async/await is available
local project_root, project = reload_and_get_git_project(node.absolute_path)
require("nvim-tree.explorer.reload").reload(parent_node, project)
update_parent_statuses(parent_node, project, project_root)
end
end end
---Refresh contents and git status for all nodes to a path: actual directory and links ---Refresh contents and git status for all nodes to a path: actual directory and links

View File

@ -59,8 +59,9 @@ function M.create_watcher(node)
else else
log.line("watcher", "node event executing refresh '%s'", node.absolute_path) log.line("watcher", "node event executing refresh '%s'", node.absolute_path)
end end
require("nvim-tree.explorer.reload").refresh_node(node) require("nvim-tree.explorer.reload").refresh_node(node, function()
require("nvim-tree.renderer").draw() require("nvim-tree.renderer").draw()
end)
end) end)
end end

View File

@ -22,36 +22,7 @@ local WATCHED_FILES = {
"index", -- staging area "index", -- staging area
} }
function M.reload() local function reload_git_status(project_root, path, project, git_status)
if not M.config.git.enable then
return {}
end
for project_root in pairs(M.projects) do
M.reload_project(project_root)
end
return M.projects
end
function M.reload_project(project_root, path)
local project = M.projects[project_root]
if not project or not M.config.git.enable then
return
end
if path and path:find(project_root, 1, true) ~= 1 then
return
end
local git_status = Runner.run {
project_root = project_root,
path = path,
list_untracked = git_utils.should_show_untracked(project_root),
list_ignored = true,
timeout = M.config.git.timeout,
}
if path then if path then
for p in pairs(project.files) do for p in pairs(project.files) do
if p:find(path, 1, true) == 1 then if p:find(path, 1, true) == 1 then
@ -66,6 +37,54 @@ function M.reload_project(project_root, path)
project.dirs = git_utils.file_status_to_dir_status(project.files, project_root) project.dirs = git_utils.file_status_to_dir_status(project.files, project_root)
end end
function M.reload()
if not M.config.git.enable then
return {}
end
for project_root in pairs(M.projects) do
M.reload_project(project_root)
end
return M.projects
end
function M.reload_project(project_root, path, callback)
local project = M.projects[project_root]
if not project or not M.config.git.enable then
if callback then
callback()
end
return
end
if path and path:find(project_root, 1, true) ~= 1 then
if callback then
callback()
end
return
end
local opts = {
project_root = project_root,
path = path,
list_untracked = git_utils.should_show_untracked(project_root),
list_ignored = true,
timeout = M.config.git.timeout,
}
if callback then
Runner.run(opts, function(git_status)
reload_git_status(project_root, path, project, git_status)
callback()
end)
else
-- TODO use callback once async/await is available
local git_status = Runner.run(opts)
reload_git_status(project_root, path, project, git_status)
end
end
function M.get_project(project_root) function M.get_project(project_root)
return M.projects[project_root] return M.projects[project_root]
end end
@ -103,21 +122,22 @@ local function reload_tree_at(project_root)
return return
end end
M.reload_project(project_root) M.reload_project(project_root, nil, function()
local git_status = M.get_project(project_root) local git_status = M.get_project(project_root)
Iterator.builder(root_node.nodes) Iterator.builder(root_node.nodes)
:hidden() :hidden()
:applier(function(node) :applier(function(node)
local parent_ignored = explorer_node.is_git_ignored(node.parent) local parent_ignored = explorer_node.is_git_ignored(node.parent)
explorer_node.update_git_status(node, parent_ignored, git_status) explorer_node.update_git_status(node, parent_ignored, git_status)
end) end)
:recursor(function(node) :recursor(function(node)
return node.nodes and #node.nodes > 0 and node.nodes return node.nodes and #node.nodes > 0 and node.nodes
end) end)
:iterate() :iterate()
require("nvim-tree.renderer").draw() require("nvim-tree.renderer").draw()
end)
end end
function M.load_project_status(cwd) function M.load_project_status(cwd)

View File

@ -69,7 +69,7 @@ function Runner:_log_raw_output(output)
end end
end end
function Runner:_run_git_job() function Runner:_run_git_job(callback)
local handle, pid local handle, pid
local stdout = vim.loop.new_pipe(false) local stdout = vim.loop.new_pipe(false)
local stderr = vim.loop.new_pipe(false) local stderr = vim.loop.new_pipe(false)
@ -78,6 +78,9 @@ function Runner:_run_git_job()
local function on_finish(rc) local function on_finish(rc)
self.rc = rc or 0 self.rc = rc or 0
if timer:is_closing() or stdout:is_closing() or stderr:is_closing() or (handle and handle:is_closing()) then if timer:is_closing() or stdout:is_closing() or stderr:is_closing() or (handle and handle:is_closing()) then
if callback then
callback()
end
return return
end end
timer:stop() timer:stop()
@ -91,6 +94,10 @@ function Runner:_run_git_job()
end end
pcall(vim.loop.kill, pid) pcall(vim.loop.kill, pid)
if callback then
callback()
end
end end
local opts = self:_getopts(stdout, stderr) local opts = self:_getopts(stdout, stderr)
@ -142,25 +149,7 @@ function Runner:_wait()
end end
end end
-- This module runs a git process, which will be killed if it takes more than timeout which defaults to 400ms function Runner:_finalise(opts)
function Runner.run(opts)
local profile = log.profile_start("git job %s %s", opts.project_root, opts.path)
local self = setmetatable({
project_root = opts.project_root,
path = opts.path,
list_untracked = opts.list_untracked,
list_ignored = opts.list_ignored,
timeout = opts.timeout or 400,
output = {},
rc = nil, -- -1 indicates timeout
}, Runner)
self:_run_git_job()
self:_wait()
log.profile_end(profile)
if self.rc == -1 then if self.rc == -1 then
log.line("git", "job timed out %s %s", opts.project_root, opts.path) log.line("git", "job timed out %s %s", opts.project_root, opts.path)
timeouts = timeouts + 1 timeouts = timeouts + 1
@ -179,8 +168,55 @@ function Runner.run(opts)
else else
log.line("git", "job success %s %s", opts.project_root, opts.path) log.line("git", "job success %s %s", opts.project_root, opts.path)
end end
end
return self.output --- Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
--- @param opts table
--- @param callback function|nil executed passing return when complete
--- @return table|nil status by absolute path, nil if callback present
function Runner.run(opts, callback)
local self = setmetatable({
project_root = opts.project_root,
path = opts.path,
list_untracked = opts.list_untracked,
list_ignored = opts.list_ignored,
timeout = opts.timeout or 400,
output = {},
rc = nil, -- -1 indicates timeout
}, Runner)
local async = callback ~= nil and self.config.git_async
local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", opts.project_root, opts.path)
if async and callback then
-- async, always call back
self:_run_git_job(function()
log.profile_end(profile)
self:_finalise(opts)
callback(self.output)
end)
else
-- sync, maybe call back
self:_run_git_job()
self:_wait()
log.profile_end(profile)
self:_finalise(opts)
if callback then
callback(self.output)
else
return self.output
end
end
end
function Runner.setup(opts)
Runner.config = {}
Runner.config.git_async = opts.experimental.git.async
end end
return Runner return Runner