diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index dfacbffeb0b..cbad8f8beff 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -408,6 +408,12 @@ Subsequent calls to setup will replace the previous configuration. watcher = false, }, }, + experimental = { + async = { + create_file = false, + rename_file = false, + } + } } -- END_DEFAULT_OPTS < @@ -1206,6 +1212,20 @@ Configuration for diagnostic logging. |nvim-tree.filesystem_watchers| processing, verbose. Type: `boolean`, Default: `false` +*nvim-tree.experimental* +Configuration for experimental features. + + *nvim-tree.experimental.async* + Control experimental async behavior. + + *nvim-tree.experimental.async.create_file* + Toggle async behavior of create file operation. + Type: `boolean`, Default: `false` + + *nvim-tree.experimental.async.rename_file* + Toggle async behavior of rename file operation. + Type: `boolean`, Default: `false` + ============================================================================== 5. API *nvim-tree-api* diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index bfe8f1e085d..e0031e3de32 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -733,6 +733,12 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS watcher = false, }, }, + experimental = { + async = { + create_file = false, + rename_file = false, + }, + }, } -- END_DEFAULT_OPTS local function merge_options(conf) diff --git a/lua/nvim-tree/actions/fs/create-file.lua b/lua/nvim-tree/actions/fs/create-file.lua index c970eeddab5..1efa01fa8ff 100644 --- a/lua/nvim-tree/actions/fs/create-file.lua +++ b/lua/nvim-tree/actions/fs/create-file.lua @@ -5,32 +5,28 @@ local core = require "nvim-tree.core" local notify = require "nvim-tree.notify" local find_file = require("nvim-tree.actions.finders.find-file").fn +local async = require "nvim-tree.async" local M = {} +---@async local function create_and_notify(file) - local ok, fd = pcall(vim.loop.fs_open, file, "w", 420) - if not ok then + local fd, err + if M.enable_async then + err, fd = async.call(vim.loop.fs_open, file, "w", 420) + else + fd, err = vim.loop.fs_open(file, "w", 420) + end + if err then notify.error("Couldn't create file " .. file) return end - vim.loop.fs_close(fd) - events._dispatch_file_created(file) -end - -local function create_file(file) - if utils.file_exists(file) then - local prompt_select = "Overwrite " .. file .. " ?" - local prompt_input = prompt_select .. " y/n: " - lib.prompt(prompt_input, prompt_select, { "y", "n" }, { "Yes", "No" }, function(item_short) - utils.clear_prompt() - if item_short == "y" then - create_and_notify(file) - end - end) + if M.enable_async then + async.call(vim.loop.fs_close, fd) else - create_and_notify(file) + vim.loop.fs_close(fd) end + events._dispatch_file_created(file) end local function get_num_nodes(iter) @@ -41,6 +37,73 @@ local function get_num_nodes(iter) return i end +local function create_file(new_file_path) + -- create a folder for each path element if the folder does not exist + -- if the answer ends with a /, create a file for the last path element + local is_last_path_file = not new_file_path:match(utils.path_separator .. "$") + local path_to_create = "" + local idx = 0 + + local num_nodes = get_num_nodes(utils.path_split(utils.path_remove_trailing(new_file_path))) + local is_error = false + for path in utils.path_split(new_file_path) do + idx = idx + 1 + local p = utils.path_remove_trailing(path) + if #path_to_create == 0 and utils.is_windows then + path_to_create = utils.path_join { p, path_to_create } + else + path_to_create = utils.path_join { path_to_create, p } + end + if is_last_path_file and idx == num_nodes then + if M.enable_async then + async.schedule() + end + if utils.file_exists(new_file_path) then + local prompt_select = "Overwrite " .. new_file_path .. " ?" + local prompt_input = prompt_select .. " y/n: " + if M.enable_async then + local item_short = async.call(lib.prompt, prompt_input, prompt_select, { "y", "n" }, { "Yes", "No" }) + utils.clear_prompt() + if item_short == "y" then + create_and_notify(new_file_path) + end + else + lib.prompt(prompt_input, prompt_select, { "y", "n" }, { "Yes", "No" }, function(item_short) + utils.clear_prompt() + if item_short == "y" then + create_and_notify(new_file_path) + end + end) + end + else + create_and_notify(new_file_path) + end + elseif not utils.file_exists(path_to_create) then + local err + if M.enable_async then + err = async.call(vim.loop.fs_mkdir, path_to_create, 493) + else + local _ + _, err = vim.loop.fs_mkdir(path_to_create, 493) + end + if err then + notify.error("Could not create folder " .. path_to_create .. ": " .. err) + is_error = true + break + end + events._dispatch_folder_created(new_file_path) + end + end + if not is_error then + notify.info(new_file_path .. " was properly created") + end + if M.enable_async then + async.schedule() + end + -- synchronously refreshes as we can't wait for the watchers + find_file(utils.path_remove_trailing(new_file_path)) +end + local function get_containing_folder(node) if node.nodes ~= nil then return utils.path_add_trailing(node.absolute_path) @@ -49,7 +112,8 @@ local function get_containing_folder(node) return node.absolute_path:sub(0, -node_name_size - 1) end -function M.fn(node) +--TODO: once async feature is finalized, use `async.wrap` instead of cb param +function M.fn(node, cb) node = node and lib.get_last_group_node(node) if not node or node.name == ".." then node = { @@ -74,45 +138,21 @@ function M.fn(node) return end - -- create a folder for each path element if the folder does not exist - -- if the answer ends with a /, create a file for the last path element - local is_last_path_file = not new_file_path:match(utils.path_separator .. "$") - local path_to_create = "" - local idx = 0 - - local num_nodes = get_num_nodes(utils.path_split(utils.path_remove_trailing(new_file_path))) - local is_error = false - for path in utils.path_split(new_file_path) do - idx = idx + 1 - local p = utils.path_remove_trailing(path) - if #path_to_create == 0 and vim.fn.has "win32" == 1 then - path_to_create = utils.path_join { p, path_to_create } - else - path_to_create = utils.path_join { path_to_create, p } - end - if is_last_path_file and idx == num_nodes then - create_file(path_to_create) - elseif not utils.file_exists(path_to_create) then - local success = vim.loop.fs_mkdir(path_to_create, 493) - if not success then - notify.error("Could not create folder " .. path_to_create) - is_error = true - break + if M.enable_async then + async.exec(create_file, new_file_path, function(err) + if cb then + cb(err) end - events._dispatch_folder_created(new_file_path) - end - end - if not is_error then - notify.info(new_file_path .. " was properly created") + end) + else + create_file(new_file_path) end - - -- synchronously refreshes as we can't wait for the watchers - find_file(utils.path_remove_trailing(new_file_path)) end) end function M.setup(opts) M.enable_reload = not opts.filesystem_watchers.enable + M.enable_async = opts.experimental.async.create_file end return M diff --git a/lua/nvim-tree/actions/fs/rename-file.lua b/lua/nvim-tree/actions/fs/rename-file.lua index 443858ececc..4fb2006c783 100644 --- a/lua/nvim-tree/actions/fs/rename-file.lua +++ b/lua/nvim-tree/actions/fs/rename-file.lua @@ -2,6 +2,7 @@ local lib = require "nvim-tree.lib" local utils = require "nvim-tree.utils" local events = require "nvim-tree.events" local notify = require "nvim-tree.notify" +local async = require "nvim-tree.async" local M = {} @@ -15,18 +16,26 @@ local function err_fmt(from, to, reason) return string.format("Cannot rename %s -> %s: %s", from, to, reason) end -function M.rename(node, to) +---@async +function M.rename(node, to, use_async) if utils.file_exists(to) then - notify.warn(err_fmt(node.absolute_path, to, "file already exists")) - return + return notify.warn(err_fmt(node.absolute_path, to, "file already exists")) end events._dispatch_will_rename_node(node.absolute_path, to) - local success, err = vim.loop.fs_rename(node.absolute_path, to) + local success, err + if use_async then + err, success = async.call(vim.loop.fs_rename, node.absolute_path, to) + else + success, err = vim.loop.fs_rename(node.absolute_path, to) + end if not success then return notify.warn(err_fmt(node.absolute_path, to, err)) end notify.info(node.absolute_path .. " ➜ " .. to) + if use_async then + async.schedule() + end utils.rename_loaded_buffers(node.absolute_path, to) events._dispatch_node_renamed(node.absolute_path, to) end @@ -34,7 +43,7 @@ end function M.fn(default_modifier) default_modifier = default_modifier or ":t" - return function(node, modifier) + return function(node, modifier, cb) if type(node) ~= "table" then node = lib.get_node_at_cursor() end @@ -76,10 +85,23 @@ function M.fn(default_modifier) if not new_file_path then return end - - M.rename(node, prepend .. new_file_path .. append) - if M.enable_reload then - require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + if M.enable_async then + async.exec(M.rename, node, prepend .. new_file_path .. append, true, function(err) + if err then + notify.error(err) + end + if M.enable_reload then + require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end + if cb then + cb(err) + end + end) + else + M.rename(node, prepend .. new_file_path .. append, false) + if M.enable_reload then + require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end end end) end @@ -87,6 +109,7 @@ end function M.setup(opts) M.enable_reload = not opts.filesystem_watchers.enable + M.enable_async = opts.experimental.async.rename_file end return M diff --git a/lua/nvim-tree/async.lua b/lua/nvim-tree/async.lua new file mode 100644 index 00000000000..df7d5e4879e --- /dev/null +++ b/lua/nvim-tree/async.lua @@ -0,0 +1,74 @@ +---Idea taken from: https://github.com/ms-jpq/lua-async-await +local co = coroutine + +local M = {} + +---@type table +local async_threads = {} + +---Execuate an asynchronous function +---@param func function +---@param ... any The arguments passed to `func`, plus a callback receives (err, ...result) +function M.exec(func, ...) + local nargs = select("#", ...) + local args = { ... } + ---@type function + local cb = function() end + if nargs > 0 then + cb = args[nargs] + args[nargs] = nil + end + + local thread = co.create(func) + async_threads[thread] = true + + local function step(...) + local res = { co.resume(thread, ...) } + local ok = table.remove(res, 1) + local err_or_next = res[1] + if co.status(thread) ~= "dead" then + local _, err = xpcall(err_or_next, debug.traceback, step) + if err then + cb(err) + end + else + async_threads[thread] = nil + if ok then + cb(nil, unpack(res)) + else + cb(debug.traceback(thread, err_or_next)) + end + end + end + + step(unpack(args)) +end + +---Test whether we are in async context +---@return boolean +function M.in_async() + local thread = co.running() + return async_threads[thread] ~= nil +end + +---Asynchronously call a function, which has callback as the last parameter (like luv apis) +---@param func function +---@param ... any +---@return any +function M.call(func, ...) + local args = { ... } + return co.yield(function(cb) + table.insert(args, cb) + func(unpack(args)) + end) +end + +---Asynchronous `vim.schedule` +---See `:h lua-loop-callbacks` and `:h api-fast`. Usually this should be used before `vim.api.*` and `vim.fn.*` calls. +function M.schedule() + return co.yield(function(cb) + vim.schedule(cb) + end) +end + +return M diff --git a/lua/nvim-tree/marks/bulk-move.lua b/lua/nvim-tree/marks/bulk-move.lua index ee0e2e8b2ac..4a1431dbcdf 100644 --- a/lua/nvim-tree/marks/bulk-move.lua +++ b/lua/nvim-tree/marks/bulk-move.lua @@ -26,7 +26,7 @@ function M.bulk_move() for _, node in pairs(marks) do local head = vim.fn.fnamemodify(node.absolute_path, ":t") local to = utils.path_join { location, head } - FsRename.rename(node, to) + FsRename.rename(node, to, false) end if M.enable_reload then