diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index 91632426c2f..b9a43a2f422 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -438,6 +438,11 @@ applying configuration. trash = true, }, }, + experimental = { + git = { + async = false, + }, + }, log = { enable = false, truncate = false, @@ -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. diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 84091b8b217..f0f372e65a4 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -636,6 +636,11 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS trash = true, }, }, + experimental = { + git = { + async = false, + }, + }, log = { enable = false, truncate = false, @@ -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) diff --git a/lua/nvim-tree/explorer/reload.lua b/lua/nvim-tree/explorer/reload.lua index b42d27cd314..31f2f835e5a 100644 --- a/lua/nvim-tree/explorer/reload.lua +++ b/lua/nvim-tree/explorer/reload.lua @@ -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) @@ -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 diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua index e80d8829176..50afb5a3da8 100644 --- a/lua/nvim-tree/explorer/watch.lua +++ b/lua/nvim-tree/explorer/watch.lua @@ -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() + require("nvim-tree.renderer").draw() + end) end) end diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua index c5be4ef02f1..a843b00c11a 100644 --- a/lua/nvim-tree/git/init.lua +++ b/lua/nvim-tree/git/init.lua @@ -22,6 +22,21 @@ local WATCHED_FILES = { "index", -- staging area } +local function reload_git_status(project_root, path, project, git_status) + 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 {} @@ -34,17 +49,23 @@ 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), @@ -52,18 +73,16 @@ function M.reload_project(project_root, path) 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) @@ -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() + 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) diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua index 6c31995b3ee..9649fa272cf 100644 --- a/lua/nvim-tree/git/runner.lua +++ b/lua/nvim-tree/git/runner.lua @@ -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) @@ -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() @@ -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) @@ -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 @@ -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