Skip to content

feat(#1974): experimental.git.async #2094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions doc/nvim-tree-lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,11 @@ applying configuration.
trash = true,
},
},
experimental = {
git = {
async = false,
},
},
log = {
enable = false,
truncate = false,
Expand Down Expand Up @@ -1222,6 +1227,16 @@ General UI configuration.
Prompt before trashing.
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*
Configuration for diagnostic logging.

Expand Down
6 changes: 6 additions & 0 deletions lua/nvim-tree.lua
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,11 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
trash = true,
},
},
experimental = {
git = {
async = false,
},
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like gitsigns has stopped (underscore prefixed) experiments.

Using the same pattern as the various lsp plugins.

log = {
enable = false,
truncate = false,
Expand Down Expand Up @@ -759,6 +764,7 @@ function M.setup(conf)
require("nvim-tree.diagnostics").setup(opts)
require("nvim-tree.explorer").setup(opts)
require("nvim-tree.git").setup(opts)
require("nvim-tree.git.runner").setup(opts)
require("nvim-tree.view").setup(opts)
require("nvim-tree.lib").setup(opts)
require("nvim-tree.renderer").setup(opts)
Expand Down
36 changes: 29 additions & 7 deletions lua/nvim-tree/explorer/reload.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ local function update_status(nodes_by_path, node_ignored, status)
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)
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

local function update_parent_statuses(node, project, root)
Expand Down Expand Up @@ -142,18 +150,32 @@ end

---Refresh contents and git status for a single node
---@param node table
function M.refresh_node(node)
function M.refresh_node(node, callback)
if type(node) ~= "table" then
if callback then
callback()
end
return
end

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

---Refresh contents and git status for all nodes to a path: actual directory and links
Expand Down
5 changes: 3 additions & 2 deletions lua/nvim-tree/explorer/watch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ function M.create_watcher(node)
else
log.line("watcher", "node event executing refresh '%s'", node.absolute_path)
end
require("nvim-tree.explorer.reload").refresh_node(node)
require("nvim-tree.renderer").draw()
require("nvim-tree.explorer.reload").refresh_node(node, function()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the last action, hence safe to use a callback.

require("nvim-tree.renderer").draw()
end)
end)
end

Expand Down
70 changes: 45 additions & 25 deletions lua/nvim-tree/git/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ local WATCHED_FILES = {
"index", -- staging area
}

local function reload_git_status(project_root, path, project, git_status)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from reload_project

if path then
for p in pairs(project.files) do
if p:find(path, 1, true) == 1 then
project.files[p] = nil
end
end
project.files = vim.tbl_deep_extend("force", project.files, git_status)
else
project.files = git_status
end

project.dirs = git_utils.file_status_to_dir_status(project.files, project_root)
end

function M.reload()
if not M.config.git.enable then
return {}
Expand All @@ -34,36 +49,40 @@ function M.reload()
return M.projects
end

function M.reload_project(project_root, path)
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 git_status = Runner.run {
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 path then
for p in pairs(project.files) do
if p:find(path, 1, true) == 1 then
project.files[p] = nil
end
end
project.files = vim.tbl_deep_extend("force", project.files, git_status)
if callback then
Runner.run(opts, function(git_status)
reload_git_status(project_root, path, project, git_status)
callback()
end)
else
project.files = git_status
-- TODO use callback once async/await is available
local git_status = Runner.run(opts)
reload_git_status(project_root, path, project, git_status)
end

project.dirs = git_utils.file_status_to_dir_status(project.files, project_root)
end

function M.get_project(project_root)
Expand Down Expand Up @@ -103,21 +122,22 @@ local function reload_tree_at(project_root)
return
end

M.reload_project(project_root)
local git_status = M.get_project(project_root)
M.reload_project(project_root, nil, function()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safe to call back as this is the last operation of the watcher.

local git_status = M.get_project(project_root)

Iterator.builder(root_node.nodes)
:hidden()
:applier(function(node)
local parent_ignored = explorer_node.is_git_ignored(node.parent)
explorer_node.update_git_status(node, parent_ignored, git_status)
end)
:recursor(function(node)
return node.nodes and #node.nodes > 0 and node.nodes
end)
:iterate()
Iterator.builder(root_node.nodes)
:hidden()
:applier(function(node)
local parent_ignored = explorer_node.is_git_ignored(node.parent)
explorer_node.update_git_status(node, parent_ignored, git_status)
end)
:recursor(function(node)
return node.nodes and #node.nodes > 0 and node.nodes
end)
:iterate()

require("nvim-tree.renderer").draw()
require("nvim-tree.renderer").draw()
end)
end

function M.load_project_status(cwd)
Expand Down
78 changes: 57 additions & 21 deletions lua/nvim-tree/git/runner.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function Runner:_log_raw_output(output)
end
end

function Runner:_run_git_job()
function Runner:_run_git_job(callback)
local handle, pid
local stdout = vim.loop.new_pipe(false)
local stderr = vim.loop.new_pipe(false)
Expand All @@ -78,6 +78,9 @@ function Runner:_run_git_job()
local function on_finish(rc)
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 callback then
callback()
end
return
end
timer:stop()
Expand All @@ -91,6 +94,10 @@ function Runner:_run_git_job()
end

pcall(vim.loop.kill, pid)

if callback then
callback()
end
end

local opts = self:_getopts(stdout, stderr)
Expand Down Expand Up @@ -142,25 +149,7 @@ function Runner:_wait()
end
end

-- This module runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
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)

function Runner:_finalise(opts)
if self.rc == -1 then
log.line("git", "job timed out %s %s", opts.project_root, opts.path)
timeouts = timeouts + 1
Expand All @@ -179,8 +168,55 @@ function Runner.run(opts)
else
log.line("git", "job success %s %s", opts.project_root, opts.path)
end
end

--- 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

return self.output
function Runner.setup(opts)
Runner.config = {}
Runner.config.git_async = opts.experimental.git.async
end

return Runner