diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index 9c4b33f3ca7..59d11d2783a 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -63,7 +63,7 @@ Requirements ============================================================================== 2. QUICK START *nvim-tree-quickstart* -Setup should be run in a lua file or in a |lua-heredoc| if using in a vim file. +Setup should be run in a lua file or in a |lua-heredoc| if using in a vim file. >lua -- examples for your init.lua @@ -168,7 +168,7 @@ setup() function takes one optional argument: configuration table. If omitted nvim-tree will be initialised with default configuration. Subsequent calls to setup will replace the previous configuration. -> +>lua require("nvim-tree").setup { -- BEGIN_DEFAULT_OPTS auto_reload_on_write = true, disable_netrw = false, @@ -393,6 +393,14 @@ Subsequent calls to setup will replace the previous configuration. watcher = false, }, }, + experimental = { + async = { + copy_paste = false, + create_file = false, + remove_file = false, + rename_file = false, + } + } } -- END_DEFAULT_OPTS < @@ -404,7 +412,7 @@ Completely disable netrw It is strongly advised to eagerly disable netrw, due to race conditions at vim startup. -Set the following at the very beginning of your `init.lua` / `init.vim`: > +Set the following at the very beginning of your `init.lua` / `init.vim`: >lua vim.g.loaded_netrw = 1 vim.g.loaded_netrwPlugin = 1 < @@ -451,7 +459,7 @@ function. - `name`: `string` - `type`: `"directory"` | `"file"` | `"link"` - Example: sort by name length: > + Example: sort by name length: >lua local sort_by = function(nodes) table.sort(nodes, function(a, b) return #a.name < #b.name @@ -645,7 +653,7 @@ This can be used to attach keybindings to the tree buffer. When on_attach is "disabled", it will use the older mapping strategy, otherwise it will use the newer one. Type: `function(bufnr)`, Default: `"disable"` - e.g. > + e.g. >lua local api = require("nvim-tree.api") local function on_attach(bufnr) @@ -779,7 +787,7 @@ UI rendering setup Type: `string` or `function(root_cwd)`, Default: `":~:s?$?/..?"` Function is passed the absolute path of the root folder and should return a string. - e.g. > + e.g. >lua my_root_folder_label = function(path) return ".../" .. vim.fn.fnamemodify(path, ":t") end @@ -1011,7 +1019,7 @@ Configuration for various actions. The function should return the window id that will open the node, or `nil` if an invalid window is picked or user cancelled the action. Type: `string` | `function`, Default: `"default"` - e.g. s1n7ax/nvim-window-picker plugin: > + e.g. s1n7ax/nvim-window-picker plugin: >lua window_picker = { enable = true, picker = require('window-picker').pick_window, @@ -1133,6 +1141,18 @@ 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.copy_paste* + Toggle async behavior of copy paste operation. + Type: `boolean`, Default: `false` + + TODO here + ============================================================================== 4.1 VINEGAR STYLE *nvim-tree-vinegar* @@ -1143,8 +1163,8 @@ it in a specific way: - Use `require"nvim-tree".open_replacing_current_buffer()` instead of the default open command. -You can easily implement a toggle using this too: -> +You can easily implement a toggle using this too: >lua + local function toggle_replace() local view = require"nvim-tree.view" local api = require"nvim-tree.api" @@ -1156,8 +1176,8 @@ You can easily implement a toggle using this too: end < - Use the `edit_in_place` action to edit files. It's bound to `` by -default, vinegar uses ``. You can override this with: -> +default, vinegar uses ``. You can override this with: >lua + require"nvim-tree".setup { view = { mappings = { @@ -1178,8 +1198,8 @@ A good functionality to enable is |nvim-tree.hijack_directories|. 5. API *nvim-tree-api* Nvim-tree's public API can be used to access features. -> -e.g. > + +e.g. >lua local api = require("nvim-tree.api") api.tree.toggle() < @@ -1299,7 +1319,7 @@ Setting your own mapping in the configuration will soon be deprecated, see Default `'n'`. Examples: -> +>lua local function print_node_path(node) print(node.absolute_path) end @@ -1381,7 +1401,7 @@ DEFAULT MAPPINGS *nvim-tree-default-mappings `m` toggle_mark Toggle node in bookmarks `bmv` bulk_move Move all bookmarked nodes into specified location -> +>lua view.mappings.list = { -- BEGIN_DEFAULT_MAPPINGS { key = { "", "o", "<2-LeftMouse>" }, action = "edit" }, { key = "", action = "edit_in_place" }, @@ -1443,7 +1463,7 @@ All the following highlight groups can be configured by hand. Aside from groups. Example (in your `init.vim`): -> +>vim highlight NvimTreeSymlink guifg=blue gui=bold,underline < You should have 'termguicolors' enabled, otherwise, colors will not be @@ -1533,7 +1553,7 @@ to |nvim_tree_registering_handlers| for more information. Handlers are registered by calling |nvim-tree-api| `events.subscribe` function with an `events.Event` kind. -e.g. handler for node renamed: > +e.g. handler for node renamed: >lua local api = require("nvim-tree.api") local Event = api.events.Event @@ -1594,11 +1614,10 @@ To get the list of marked paths, you can call Navigation for marks is not bound by default in nvim-tree because we don't want to focus the tree view each time we wish to switch to another mark. -This requires binding bookmark navigation yourself. - --- in your lua configuration -vim.keymap.set("n", "mn", require("nvim-tree.api").marks.navigate.next) -vim.keymap.set("n", "mp", require("nvim-tree.api").marks.navigate.prev) -vim.keymap.set("n", "ms", require("nvim-tree.api").marks.navigate.select) - +This requires binding bookmark navigation yourself. >lua + -- in your lua configuration + vim.keymap.set("n", "mn", require("nvim-tree.api").marks.navigate.next) + vim.keymap.set("n", "mp", require("nvim-tree.api").marks.navigate.prev) + vim.keymap.set("n", "ms", require("nvim-tree.api").marks.navigate.select) +< vim:tw=78:ts=4:sw=4:et:ft=help:norl: diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 63863a3dd90..9902ad27604 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -702,6 +702,14 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS watcher = false, }, }, + experimental = { + async = { + copy_paste = false, + create_file = false, + remove_file = false, + rename_file = false, + }, + }, } -- END_DEFAULT_OPTS local function merge_options(conf) diff --git a/lua/nvim-tree/actions/fs/copy-paste/async.lua b/lua/nvim-tree/actions/fs/copy-paste/async.lua new file mode 100644 index 00000000000..44e890ea794 --- /dev/null +++ b/lua/nvim-tree/actions/fs/copy-paste/async.lua @@ -0,0 +1,301 @@ +local lib = require "nvim-tree.lib" +local log = require "nvim-tree.log" +local utils = require "nvim-tree.utils" +local core = require "nvim-tree.core" +local events = require "nvim-tree.events" +local notify = require "nvim-tree.notify" +local async = require "nvim-tree.async" + +local M = {} + +local clipboard = { + move = {}, + copy = {}, +} + +local function do_copy_dir(source, destination, mode) + local success + local errmsg, handle = async.call(function(cb) + return vim.loop.fs_opendir(source, cb, 32) + end) + if not handle then + log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, errmsg) + return false, errmsg + end + + local _, stats = async.call(vim.loop.fs_stat, destination) + if stats then + errmsg, success = async.call(vim.loop.fs_chmod, destination, mode) + if not success then + log.line("copy_paste", "do_copy fs_chmod '%s' failed '%s'", destination, errmsg) + -- ignore error and continue, the dir exists + end + else + errmsg = async.call(vim.loop.fs_mkdir, destination, mode) + if errmsg then + log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, errmsg) + return false, errmsg + end + end + + while true do + local _, entries = async.call(vim.loop.fs_readdir, handle) + if not entries or #entries == 0 then + break + end + local cp_tasks = {} + for _, entry in pairs(entries) do + local name = entry.name + local type = entry.type + local new_name = utils.path_join { source, name } + local new_destination = utils.path_join { destination, name } + + errmsg, stats = async.call(vim.loop.fs_stat, new_name) + if errmsg then + log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", new_name, errmsg) + else + if type == "directory" then + success, errmsg = do_copy_dir(new_name, new_destination, stats.mode) + if not success then + return false, errmsg + end + else + table.insert(cp_tasks, function() + errmsg, success = async.call(vim.loop.fs_copyfile, new_name, new_destination, nil) + if not success then + log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg) + return false, errmsg + end + return true + end) + end + end + end + for _, result in ipairs(async.all(unpack(cp_tasks))) do + _, success = unpack(result) + if not success then + return false + end + end + end + return true +end + +local function do_copy(source, destination) + local source_stats, success, errmsg + + source_stats, errmsg = vim.loop.fs_stat(source) + if not source_stats then + log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, errmsg) + return false, errmsg + end + + log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination) + + if source == destination then + log.line("copy_paste", "do_copy source and destination are the same, exiting early") + return true + end + + if source_stats.type == "file" then + errmsg, success = async.call(vim.loop.fs_copyfile, source, destination) + if not success then + log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg) + return false, errmsg + end + elseif source_stats.type == "directory" then + success, errmsg = do_copy_dir(source, destination, source_stats.mode) + if not success then + return false, errmsg + end + else + errmsg = string.format("'%s' illegal file type '%s'", source, source_stats.type) + log.line("copy_paste", "do_copy %s", errmsg) + return false, errmsg + end + + return true +end + +local function do_single_paste(source, dest, action_type, action_fn) + local dest_stats + local success, errmsg, errcode + + log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest) + + dest_stats, errmsg, errcode = vim.loop.fs_stat(dest) + if not dest_stats and errcode ~= "ENOENT" then + notify.error("Could not " .. action_type .. " " .. source .. " - " .. (errmsg or "???")) + return false, errmsg + end + + local function on_process() + success, errmsg = action_fn(source, dest) + if not success then + notify.error("Could not " .. action_type .. " " .. source .. " - " .. (errmsg or "???")) + return false, errmsg + end + end + + if dest_stats then + local prompt_select = "Overwrite " .. dest .. " ?" + -- TODO: buffer with name `dest` + local prompt_input = prompt_select .. " y/n/r(ename): " + local item_short = async.call(lib.prompt, prompt_input, prompt_select, { "y", "n", "r" }, { "Yes", "No", "Rename" }) + utils.clear_prompt() + if item_short == "y" then + on_process() + elseif item_short == "r" then + local new_dest = async.call(vim.ui.input, { prompt = "Rename to ", default = dest, completion = "dir" }) + utils.clear_prompt() + if new_dest then + do_single_paste(source, new_dest, action_type, action_fn) + end + end + else + on_process() + end +end + +local function add_to_clipboard(node, clip) + if node.name == ".." then + return + end + + for idx, _node in ipairs(clip) do + if _node.absolute_path == node.absolute_path then + table.remove(clip, idx) + return notify.info(node.absolute_path .. " removed to clipboard.") + end + end + table.insert(clip, node) + notify.info(node.absolute_path .. " added to clipboard.") +end + +function M.clear_clipboard() + clipboard.move = {} + clipboard.copy = {} + notify.info "Clipboard has been emptied." +end + +function M.copy(node) + add_to_clipboard(node, clipboard.copy) +end + +function M.cut(node) + add_to_clipboard(node, clipboard.move) +end + +local function do_paste(node, action_type, action_fn) + node = lib.get_last_group_node(node) + if node.name == ".." then + node = core.get_explorer() + end + local clip = clipboard[action_type] + if #clip == 0 then + return + end + + local destination = node.absolute_path + local stats, errmsg, errcode = vim.loop.fs_stat(destination) + if not stats and errcode ~= "ENOENT" then + log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, errmsg) + notify.error("Could not " .. action_type .. " " .. destination .. " - " .. (errmsg or "???")) + return + end + local is_dir = stats and stats.type == "directory" + if not is_dir then + destination = vim.fn.fnamemodify(destination, ":p:h") + end + + for _, _node in ipairs(clip) do + local dest = utils.path_join { destination, _node.name } + do_single_paste(_node.absolute_path, dest, action_type, action_fn) + end + + clipboard[action_type] = {} + if M.enable_reload then + return require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end +end + +local function do_cut(source, destination) + log.line("copy_paste", "do_cut '%s' -> '%s'", source, destination) + + if source == destination then + log.line("copy_paste", "do_cut source and destination are the same, exiting early") + return true + end + + local errmsg, success = async.call(vim.loop.fs_rename, source, destination) + if not success then + log.line("copy_paste", "do_cut fs_rename failed '%s'", errmsg) + return false, errmsg + end + async.schedule() + utils.rename_loaded_buffers(source, destination) + events._dispatch_node_renamed(source, destination) + return true +end + +M.paste = async.wrap(function(node) + if clipboard.move[1] ~= nil then + return do_paste(node, "move", do_cut) + end + + return do_paste(node, "copy", do_copy) +end, 1) + +function M.print_clipboard() + local content = {} + if #clipboard.move > 0 then + table.insert(content, "Cut") + for _, item in pairs(clipboard.move) do + table.insert(content, " * " .. item.absolute_path) + end + end + if #clipboard.copy > 0 then + table.insert(content, "Copy") + for _, item in pairs(clipboard.copy) do + table.insert(content, " * " .. item.absolute_path) + end + end + + return notify.info(table.concat(content, "\n") .. "\n") +end + +local function copy_to_clipboard(content) + if M.use_system_clipboard == true then + vim.fn.setreg("+", content) + vim.fn.setreg('"', content) + return notify.info(string.format("Copied %s to system clipboard!", content)) + else + vim.fn.setreg('"', content) + vim.fn.setreg("1", content) + return notify.info(string.format("Copied %s to neovim clipboard!", content)) + end +end + +function M.copy_filename(node) + return copy_to_clipboard(node.name) +end + +function M.copy_path(node) + local absolute_path = node.absolute_path + local relative_path = utils.path_relative(absolute_path, core.get_cwd()) + local content = node.nodes ~= nil and utils.path_add_trailing(relative_path) or relative_path + return copy_to_clipboard(content) +end + +function M.copy_absolute_path(node) + local absolute_path = node.absolute_path + local content = node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path + return copy_to_clipboard(content) +end + +function M.setup(opts) + M.use_system_clipboard = opts.actions.use_system_clipboard + M.enable_reload = not opts.filesystem_watchers.enable +end + +return M diff --git a/lua/nvim-tree/actions/fs/copy-paste.lua b/lua/nvim-tree/actions/fs/copy-paste/init.lua similarity index 91% rename from lua/nvim-tree/actions/fs/copy-paste.lua rename to lua/nvim-tree/actions/fs/copy-paste/init.lua index fdbee38c42c..282ba288a43 100644 --- a/lua/nvim-tree/actions/fs/copy-paste.lua +++ b/lua/nvim-tree/actions/fs/copy-paste/init.lua @@ -4,6 +4,7 @@ local utils = require "nvim-tree.utils" local core = require "nvim-tree.core" local events = require "nvim-tree.events" local notify = require "nvim-tree.notify" +local async = require "nvim-tree.actions.fs.copy-paste.async" local M = {} @@ -130,16 +131,25 @@ local function add_to_clipboard(node, clip) end function M.clear_clipboard() + if M.enable_async then + return async.clear_clipboard() + end clipboard.move = {} clipboard.copy = {} notify.info "Clipboard has been emptied." end function M.copy(node) + if M.enable_async then + return async.copy(node) + end add_to_clipboard(node, clipboard.copy) end function M.cut(node) + if M.enable_async then + return async.cut(node) + end add_to_clipboard(node, clipboard.move) end @@ -195,7 +205,10 @@ local function do_cut(source, destination) return true end -function M.paste(node) +function M.paste(node, cb) + if M.enable_async then + return async.paste(node, cb) + end if clipboard.move[1] ~= nil then return do_paste(node, "move", do_cut) end @@ -204,6 +217,9 @@ function M.paste(node) end function M.print_clipboard() + if M.enable_async then + return async.print_clipboard() + end local content = {} if #clipboard.move > 0 then table.insert(content, "Cut") @@ -234,10 +250,16 @@ local function copy_to_clipboard(content) end function M.copy_filename(node) + if M.enable_async then + return async.copy_filename(node) + end return copy_to_clipboard(node.name) end function M.copy_path(node) + if M.enable_async then + return async.copy_path(node) + end local absolute_path = node.absolute_path local relative_path = utils.path_relative(absolute_path, core.get_cwd()) local content = node.nodes ~= nil and utils.path_add_trailing(relative_path) or relative_path @@ -245,6 +267,9 @@ function M.copy_path(node) end function M.copy_absolute_path(node) + if M.enable_async then + return async.copy_absolute_path(node) + end local absolute_path = node.absolute_path local content = node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path return copy_to_clipboard(content) @@ -253,6 +278,8 @@ end function M.setup(opts) M.use_system_clipboard = opts.actions.use_system_clipboard M.enable_reload = not opts.filesystem_watchers.enable + M.enable_async = opts.experimental.async.copy_paste + async.setup(opts) end return M diff --git a/lua/nvim-tree/actions/fs/create-file.lua b/lua/nvim-tree/actions/fs/create-file.lua index c970eeddab5..ecac4f03c66 100644 --- a/lua/nvim-tree/actions/fs/create-file.lua +++ b/lua/nvim-tree/actions/fs/create-file.lua @@ -5,16 +5,27 @@ 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 = {} 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) + if M.enable_async then + async.call(vim.loop.fs_close, fd) + async.schedule() + else + vim.loop.fs_close(fd) + end events._dispatch_file_created(file) end @@ -49,7 +60,61 @@ local function get_containing_folder(node) return node.absolute_path:sub(0, -node_name_size - 1) end -function M.fn(node) +local async_fn = async.wrap(function(node) + local containing_folder = get_containing_folder(node) + + local input_opts = { prompt = "Create file ", default = containing_folder, completion = "file" } + + local new_file_path = async.call(vim.ui.input, input_opts) + utils.clear_prompt() + if not new_file_path or new_file_path == containing_folder then + return + end + + if utils.file_exists(new_file_path) then + notify.warn "Cannot create: file already exists" + 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) + async.schedule() + 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 err = async.call(vim.loop.fs_mkdir, path_to_create, 493) + if err then + notify.error("Could not create folder " .. path_to_create .. " :" .. err) + is_error = true + break + end + async.schedule() + events._dispatch_folder_created(new_file_path) + end + end + if not is_error then + notify.info(new_file_path .. " was properly created") + end + + -- synchronously refreshes as we can't wait for the watchers + find_file(utils.path_remove_trailing(new_file_path)) +end, 1) + +function M.fn(node, cb) node = node and lib.get_last_group_node(node) if not node or node.name == ".." then node = { @@ -59,6 +124,10 @@ function M.fn(node) } end + if M.enable_async then + return async_fn(node, cb) + end + local containing_folder = get_containing_folder(node) local input_opts = { prompt = "Create file ", default = containing_folder, completion = "file" } @@ -113,6 +182,7 @@ 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/remove-file.lua b/lua/nvim-tree/actions/fs/remove-file.lua index ff2d4630041..ef06e283ef3 100644 --- a/lua/nvim-tree/actions/fs/remove-file.lua +++ b/lua/nvim-tree/actions/fs/remove-file.lua @@ -3,6 +3,7 @@ local events = require "nvim-tree.events" local view = require "nvim-tree.view" local lib = require "nvim-tree.lib" local notify = require "nvim-tree.notify" +local async = require "nvim-tree.async" local M = {} @@ -69,6 +70,82 @@ local function remove_dir(cwd) return vim.loop.fs_rmdir(cwd) end +local function do_remove(node) + if node.nodes ~= nil and not node.link_to then + local success = remove_dir(node.absolute_path) + if not success then + return notify.error("Could not remove " .. node.name) + end + events._dispatch_folder_removed(node.absolute_path) + else + local success = vim.loop.fs_unlink(node.absolute_path) + if not success then + return notify.error("Could not remove " .. node.name) + end + events._dispatch_file_removed(node.absolute_path) + clear_buffer(node.absolute_path) + end + notify.info(node.absolute_path .. " was properly removed.") + if M.enable_reload then + require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end +end + +local function remove_dir_async(cwd) + local handle = async.unwrap_err(async.call(function(cb) + return vim.loop.fs_opendir(cwd, cb, 32) + end)) + while true do + local _, entries = async.call(vim.loop.fs_readdir, handle) + if not entries or #entries == 0 then + break + end + local tasks = {} + for _, entry in pairs(entries or {}) do + async.schedule() + local name = entry.name + local t = entry.type + local new_cwd = utils.path_join { cwd, name } + if t == "directory" then + remove_dir_async(new_cwd) + else + table.insert(tasks, function() + async.unwrap_err(async.call(vim.loop.fs_unlink, new_cwd)) + async.schedule() + clear_buffer(new_cwd) + end) + end + end + async.all(unpack(tasks)) + end + + async.unwrap_err(async.call(vim.loop.fs_rmdir, cwd)) +end + +local function do_remove_async(node) + async.exec(function() + if node.nodes ~= nil and not node.link_to then + remove_dir_async(node.absolute_path) + async.schedule() + events._dispatch_folder_removed(node.absolute_path) + else + async.unwrap_err(async.call(vim.loop.fs_unlink, node.absolute_path)) + async.schedule() + events._dispatch_file_removed(node.absolute_path) + clear_buffer(node.absolute_path) + end + end, function(err) + if err then + notify.error("Could not remove " .. node.name .. ": " .. tostring(err)) + else + notify.info(node.absolute_path .. " was properly removed.") + if M.enable_reload then + require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end + end + end) +end + function M.fn(node) if node.name == ".." then return @@ -78,23 +155,10 @@ function M.fn(node) lib.prompt(prompt_input, prompt_select, { "y", "n" }, { "Yes", "No" }, function(item_short) utils.clear_prompt() if item_short == "y" then - if node.nodes ~= nil and not node.link_to then - local success = remove_dir(node.absolute_path) - if not success then - return notify.error("Could not remove " .. node.name) - end - events._dispatch_folder_removed(node.absolute_path) + if M.enable_async then + do_remove_async(node) else - local success = vim.loop.fs_unlink(node.absolute_path) - if not success then - return notify.error("Could not remove " .. node.name) - end - events._dispatch_file_removed(node.absolute_path) - clear_buffer(node.absolute_path) - end - notify.info(node.absolute_path .. " was properly removed.") - if M.enable_reload then - require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + do_remove(node) end end end) @@ -103,6 +167,7 @@ end function M.setup(opts) M.enable_reload = not opts.filesystem_watchers.enable M.close_window = opts.actions.remove_file.close_window + M.enable_async = opts.experimental.async.remove_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..1861b6cae42 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 = {} @@ -22,11 +23,19 @@ function M.rename(node, to) 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 M.enable_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 M.enable_async then + async.schedule() + end utils.rename_loaded_buffers(node.absolute_path, to) events._dispatch_node_renamed(node.absolute_path, to) end @@ -76,10 +85,20 @@ 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, function(err) + if err then + notify.error(err) + end + if M.enable_reload then + require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end + end) + else + M.rename(node, prepend .. new_file_path .. append) + if M.enable_reload then + require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end end end) end @@ -87,6 +106,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/actions/node/open-file.lua b/lua/nvim-tree/actions/node/open-file.lua index ac01fe57677..46cd5bd37c9 100644 --- a/lua/nvim-tree/actions/node/open-file.lua +++ b/lua/nvim-tree/actions/node/open-file.lua @@ -119,15 +119,19 @@ local function pick_win_id() -- Restore window options for _, id in ipairs(selectable) do - for opt, value in pairs(win_opts[id]) do - vim.api.nvim_win_set_option(id, opt, value) + if vim.api.nvim_win_is_valid(id) then + for opt, value in pairs(win_opts[id]) do + vim.api.nvim_win_set_option(id, opt, value) + end end end if laststatus == 3 then for _, id in ipairs(not_selectable) do - for opt, value in pairs(win_opts[id]) do - vim.api.nvim_win_set_option(id, opt, value) + if vim.api.nvim_win_is_valid(id) then + for opt, value in pairs(win_opts[id]) do + vim.api.nvim_win_set_option(id, opt, value) + end end end end diff --git a/lua/nvim-tree/async.lua b/lua/nvim-tree/async.lua new file mode 100644 index 00000000000..9f7f748e2aa --- /dev/null +++ b/lua/nvim-tree/async.lua @@ -0,0 +1,126 @@ +---Idea taken from: https://github.com/ms-jpq/lua-async-await +local co = coroutine + +local M = {} + +---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 args = { ... } + local cb = table.remove(args) or function() end + local thread = co.create(func) + + 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 + elseif ok then + cb(nil, unpack(res)) + else + cb(debug.traceback(thread, err_or_next)) + end + end + + step(...) +end + +---Wrap an asynchronous function to be directly called in synchronous context +---@param func function +---@param args_count number The number of arguments the wrapped function accepts. Pass it if you want the returned function to receive an additional callback as final argument, and the signature of callback is the same as that of `async.exec`. +---@return function +function M.wrap(func, args_count) + return function(...) + local args = { ... } + if args_count == nil or #args == args_count then + table.insert(args, function() end) + end + M.exec(func, unpack(args)) + end +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 + +---Execuate multiple asynchronous function simultaneously +---@param ... fun() +---@return table[] (err, ...result) tuples from every function +function M.all(...) + local tasks = { ... } + if #tasks == 0 then + return {} + end + + local results = {} + local finished = 0 + return co.yield(function(cb) + for i, task in ipairs(tasks) do + M.exec(task, function(...) + finished = finished + 1 + results[i] = { ... } + if finished == #tasks then + cb(results) + end + end) + end + end) +end + +---Asynchronous `vim.schedule` +function M.schedule() + return co.yield(function(cb) + vim.schedule(cb) + end) +end + +function M.unwrap_err(...) + local args = { ... } + local err = table.remove(args, 1) + if err then + error(err) + end + return unpack(args) +end + +---@class Interrupter +---@field yield fun() +---@field interval number +---@field last number +local Interrupter = {} + +---@return Interrupter +function Interrupter.new(ms, yield) + local obj = { + interval = ms or 12, + last = vim.loop.hrtime(), + yield = yield or M.schedule, + } + setmetatable(obj, { __index = Interrupter }) + return obj +end + +function Interrupter:check() + local cur = vim.loop.hrtime() + if cur - self.last >= self.interval * 1000000 then + self:yield() + self.last = cur + end +end + +M.Interrupter = Interrupter + +return M diff --git a/lua/nvim-tree/iterators/node-iterator.lua b/lua/nvim-tree/iterators/node-iterator.lua index e4a1d0bb0fc..f6b1de5f6ea 100644 --- a/lua/nvim-tree/iterators/node-iterator.lua +++ b/lua/nvim-tree/iterators/node-iterator.lua @@ -1,3 +1,4 @@ +local async = require "nvim-tree.async" local NodeIterator = {} NodeIterator.__index = NodeIterator @@ -62,4 +63,31 @@ function NodeIterator:iterate() return iter(self.nodes) end +function NodeIterator:async_iterate() + local interrupter = async.Interrupter.new() + local iteration_count = 0 + local function iter(nodes) + for _, node in ipairs(nodes) do + interrupter:check() + if self._filter_hidden(node) then + iteration_count = iteration_count + 1 + if self._match(node) then + return node, iteration_count + end + self._apply_fn_on_node(node, iteration_count) + local children = self._recurse_with(node) + if children then + local n = iter(children) + if n then + return n, iteration_count + end + end + end + end + return nil, 0 + end + + return iter(self.nodes) +end + return NodeIterator diff --git a/lua/nvim-tree/marks/bulk-move.lua b/lua/nvim-tree/marks/bulk-move.lua index ee0e2e8b2ac..0aedcfa5213 100644 --- a/lua/nvim-tree/marks/bulk-move.lua +++ b/lua/nvim-tree/marks/bulk-move.lua @@ -3,6 +3,7 @@ local Core = require "nvim-tree.core" local utils = require "nvim-tree.utils" local FsRename = require "nvim-tree.actions.fs.rename-file" local notify = require "nvim-tree.notify" +local async = require "nvim-tree.async" local M = {} @@ -23,20 +24,37 @@ function M.bulk_move() end local marks = Marks.get_marks() - 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) - end + if M.enable_async then + async.exec(function() + for _, node in pairs(marks) do + async.schedule() + local head = vim.fn.fnamemodify(node.absolute_path, ":t") + local to = utils.path_join { location, head } + FsRename.rename(node, to) + end + + if M.enable_reload then + require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end + end) + else + 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) + end - if M.enable_reload then - require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + if M.enable_reload then + require("nvim-tree.actions.reloaders.reloaders").reload_explorer() + end end end) end function M.setup(opts) M.enable_reload = not opts.filesystem_watchers.enable + -- if rename_file is async, bulk_remove is also async + M.enable_async = opts.experimental.async.rename_file end return M