1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-11 10:03:39 +08:00
SpaceVim/bundle/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_actions.lua
2023-05-30 21:09:18 +08:00

592 lines
17 KiB
Lua
Vendored

-- This file is for functions that mutate the filesystem.
-- This code started out as a copy from:
-- https://github.com/mhartington/dotfiles
-- and modified to fit neo-tree's api.
-- Permalink: https://github.com/mhartington/dotfiles/blob/7560986378753e0c047d940452cb03a3b6439b11/config/nvim/lua/mh/filetree/init.lua
local vim = vim
local api = vim.api
local loop = vim.loop
local scan = require("plenary.scandir")
local utils = require("neo-tree.utils")
local inputs = require("neo-tree.ui.inputs")
local events = require("neo-tree.events")
local log = require("neo-tree.log")
local Path = require("plenary").path
local M = {}
local function clear_buffer(path)
local buf = utils.find_buffer_by_name(path)
if buf < 1 then
return
end
local alt = vim.fn.bufnr("#")
-- Check all windows to see if they are using the buffer
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == buf then
-- if there is no alternate buffer yet, create a blank one now
if alt < 1 or alt == buf then
alt = vim.api.nvim_create_buf(true, false)
end
-- replace the buffer displayed in this window with the alternate buffer
vim.api.nvim_win_set_buf(win, alt)
end
end
local success, msg = pcall(vim.api.nvim_buf_delete, buf, { force = true })
if not success then
log.error("Could not clear buffer: ", msg)
end
end
---Opens new_buf in each window that has old_buf currently open.
---Useful during file rename.
---@param old_buf number
---@param new_buf number
local function replace_buffer_in_windows(old_buf, new_buf)
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == old_buf then
vim.api.nvim_win_set_buf(win, new_buf)
end
end
end
local function rename_buffer(old_path, new_path)
local force_save = function()
vim.cmd("silent! write!")
end
for _, buf in pairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) then
local buf_name = vim.api.nvim_buf_get_name(buf)
local new_buf_name = nil
if old_path == buf_name then
new_buf_name = new_path
elseif utils.is_subpath(old_path, buf_name) then
new_buf_name = new_path .. buf_name:sub(#old_path + 1)
end
if utils.truthy(new_buf_name) then
local new_buf = vim.fn.bufadd(new_buf_name)
vim.fn.bufload(new_buf)
vim.api.nvim_buf_set_option(new_buf, "buflisted", true)
replace_buffer_in_windows(buf, new_buf)
if vim.api.nvim_buf_get_option(buf, "buftype") == "" then
local modified = vim.api.nvim_buf_get_option(buf, "modified")
if modified then
local old_buffer_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
vim.api.nvim_buf_set_lines(new_buf, 0, -1, false, old_buffer_lines)
local msg = buf_name .. " has been modified. Save under new name? (y/n) "
inputs.confirm(msg, function(confirmed)
if confirmed then
vim.api.nvim_buf_call(new_buf, force_save)
log.trace("Force saving renamed buffer with changes")
else
vim.cmd("echohl WarningMsg")
vim.cmd(
[[echo "Skipping force save. You'll need to save it with `:w!` when you are ready to force writing with the new name."]]
)
vim.cmd("echohl NONE")
end
end)
end
end
vim.api.nvim_buf_delete(buf, { force = true })
end
end
end
end
local function create_all_parents(path)
local create_all_as_folders
function create_all_as_folders(in_path)
if not loop.fs_stat(in_path) then
local parent, _ = utils.split_path(in_path)
if parent then
create_all_as_folders(parent)
end
loop.fs_mkdir(in_path, 493)
end
end
local parent_path, _ = utils.split_path(path)
create_all_as_folders(parent_path)
end
-- Gets a non-existing filename from the user and executes the callback with it.
local function get_unused_name(
destination,
using_root_directory,
name_chosen_callback,
first_message
)
if loop.fs_stat(destination) then
local parent_path, name
if not using_root_directory then
parent_path, name = utils.split_path(destination)
elseif #using_root_directory > 0 then
parent_path = destination:sub(1, #using_root_directory)
name = destination:sub(#using_root_directory + 2)
else
parent_path = nil
name = destination
end
local message = first_message or name .. " already exists. Please enter a new name: "
inputs.input(message, name, function(new_name)
if new_name and string.len(new_name) > 0 then
local new_path = parent_path and parent_path .. utils.path_separator .. new_name or new_name
get_unused_name(new_path, using_root_directory, name_chosen_callback)
end
end)
else
name_chosen_callback(destination)
end
end
-- Move Node
M.move_node = function(source, destination, callback, using_root_directory)
log.trace(
"Moving node: ",
source,
" to ",
destination,
", using root directory: ",
using_root_directory
)
local _, name = utils.split_path(source)
get_unused_name(destination or source, using_root_directory, function(dest)
local function move_file()
create_all_parents(dest)
loop.fs_rename(source, dest, function(err)
if err then
log.error("Could not move the files from", source, "to", dest, ":", err)
return
end
vim.schedule(function()
rename_buffer(source, dest)
end)
vim.schedule(function()
events.fire_event(events.FILE_MOVED, {
source = source,
destination = dest,
})
if callback then
callback(source, dest)
end
end)
end)
end
local event_result = events.fire_event(events.BEFORE_FILE_MOVE, {
source = source,
destination = dest,
callback = move_file,
}) or {}
if event_result.handled then
return
end
move_file()
end, 'Move "' .. name .. '" to:')
end
---Plenary path.copy() when used to copy a recursive structure, can return a nested
-- table with for each file a Path instance and the success result.
---@param copy_result table The output of Path.copy()
---@param flat_result table Return value containing the flattened results
local function flatten_path_copy_result(flat_result, copy_result)
if not copy_result then
return
end
for k, v in pairs(copy_result) do
if type(v) == "table" then
flatten_path_copy_result(flat_result, v)
else
table.insert(flat_result, { destination = k.filename, success = v })
end
end
end
-- Check if all files were copied successfully, using the flattened copy result
local function check_path_copy_result(flat_result)
if not flat_result then
return
end
for _, file_result in ipairs(flat_result) do
if not file_result.success then
return false
end
end
return true
end
-- Copy Node
M.copy_node = function(source, _destination, callback, using_root_directory)
local _, name = utils.split_path(source)
get_unused_name(_destination or source, using_root_directory, function(destination)
local parent_path, _ = utils.split_path(destination)
if source == parent_path then
log.warn("Cannot copy a file/folder to itself")
return
end
local source_path = Path:new(source)
if source_path:is_file() then
-- When the source is a file, then Path.copy() currently doesn't create
-- the potential non-existing parent directories of the destination.
create_all_parents(destination)
end
local success, result = pcall(source_path.copy, source_path, {
destination = destination,
recursive = true,
parents = true,
})
if not success then
log.error("Could not copy the file(s) from", source, "to", destination, ":", result)
return
end
-- It can happen that the Path.copy() function returns successfully but
-- the copy action still failed. In this case the copy() result contains
-- a nested table of Path instances for each file copied, and the success
-- result.
local flat_result = {}
flatten_path_copy_result(flat_result, result)
if not check_path_copy_result(flat_result) then
log.error("Could not copy the file(s) from", source, "to", destination, ":", flat_result)
return
end
vim.schedule(function()
events.fire_event(events.FILE_ADDED, destination)
if callback then
callback(source, destination)
end
end)
end, 'Copy "' .. name .. '" to:')
end
--- Create a new directory
M.create_directory = function(in_directory, callback, using_root_directory)
local base
if type(using_root_directory) == "string" then
if in_directory == using_root_directory then
base = ""
elseif #using_root_directory > 0 then
base = in_directory:sub(#using_root_directory + 2) .. utils.path_separator
else
base = in_directory .. utils.path_separator
end
else
base = vim.fn.fnamemodify(in_directory .. utils.path_separator, ":~")
using_root_directory = false
end
inputs.input("Enter name for new directory:", base, function(destinations)
if not destinations then
return
end
for _, destination in ipairs(utils.brace_expand(destinations)) do
if not destination or destination == base then
return
end
if using_root_directory then
destination = utils.path_join(using_root_directory, destination)
else
destination = vim.fn.fnamemodify(destination, ":p")
end
if loop.fs_stat(destination) then
log.warn("Directory already exists")
return
end
create_all_parents(destination)
loop.fs_mkdir(destination, 493)
vim.schedule(function()
events.fire_event(events.FILE_ADDED, destination)
if callback then
callback(destination)
end
end)
end
end)
end
--- Create Node
M.create_node = function(in_directory, callback, using_root_directory)
local base
if type(using_root_directory) == "string" then
if in_directory == using_root_directory then
base = ""
elseif #using_root_directory > 0 then
base = in_directory:sub(#using_root_directory + 2) .. utils.path_separator
else
base = in_directory .. utils.path_separator
end
else
base = vim.fn.fnamemodify(in_directory .. utils.path_separator, ":~")
using_root_directory = false
end
inputs.input(
'Enter name for new file or directory (dirs end with a "/"):',
base,
function(destinations)
if not destinations then
return
end
for _, destination in ipairs(utils.brace_expand(destinations)) do
if not destination or destination == base then
return
end
local is_dir = vim.endswith(destination, "/")
if using_root_directory then
destination = utils.path_join(using_root_directory, destination)
else
destination = vim.fn.fnamemodify(destination, ":p")
end
if loop.fs_stat(destination) then
log.warn("File already exists")
return
end
create_all_parents(destination)
if is_dir then
loop.fs_mkdir(destination, 493)
else
local open_mode = loop.constants.O_CREAT
+ loop.constants.O_WRONLY
+ loop.constants.O_TRUNC
local fd = loop.fs_open(destination, "w", open_mode)
if not fd then
if not loop.fs_stat(destination) then
api.nvim_err_writeln("Could not create file " .. destination)
return
else
log.warn("Failed to complete file creation of " .. destination)
end
else
loop.fs_chmod(destination, 420)
loop.fs_close(fd)
end
end
vim.schedule(function()
events.fire_event(events.FILE_ADDED, destination)
if callback then
callback(destination)
end
end)
end
end
)
end
-- Delete Node
M.delete_node = function(path, callback, noconfirm)
local _, name = utils.split_path(path)
local msg = string.format("Are you sure you want to delete '%s'?", name)
log.trace("Deleting node: ", path)
local _type = "unknown"
local stat = loop.fs_stat(path)
if stat then
_type = stat.type
if _type == "link" then
local link_to = loop.fs_readlink(path)
if not link_to then
log.error("Could not read link")
return
end
_type = loop.fs_stat(link_to)
end
if _type == "directory" then
local children = scan.scan_dir(path, {
hidden = true,
respect_gitignore = false,
add_dirs = true,
depth = 1,
})
if #children > 0 then
msg = "WARNING: Dir not empty! " .. msg
end
end
else
log.warn("Could not read file/dir:", path, stat, ", attempting to delete anyway...")
-- Guess the type by whether it appears to have an extension
if path:match("%.(.+)$") then
_type = "file"
else
_type = "directory"
end
return
end
local do_delete = function(confirmed)
if not confirmed then
return
end
local function delete_dir(dir_path)
local handle = loop.fs_scandir(dir_path)
if type(handle) == "string" then
return api.nvim_err_writeln(handle)
end
while true do
local child_name, t = loop.fs_scandir_next(handle)
if not child_name then
break
end
local child_path = dir_path .. "/" .. child_name
if t == "directory" then
local success = delete_dir(child_path)
if not success then
log.error("failed to delete ", child_path)
return false
end
else
local success = loop.fs_unlink(child_path)
if not success then
return false
end
clear_buffer(child_path)
end
end
return loop.fs_rmdir(dir_path)
end
if _type == "directory" then
-- first try using native system commands, which are recursive
local success = false
if utils.is_windows then
local result = vim.fn.system({ "cmd.exe", "/c", "rmdir", "/s", "/q", path })
local error = vim.v.shell_error
if error ~= 0 then
log.debug("Could not delete directory '", path, "' with rmdir: ", result)
else
log.info("Deleted directory ", path)
success = true
end
else
local result = vim.fn.system({ "rm", "-Rf", path })
local error = vim.v.shell_error
if error ~= 0 then
log.debug("Could not delete directory '", path, "' with rm: ", result)
else
log.info("Deleted directory ", path)
success = true
end
end
-- Fallback to using libuv if native commands fail
if not success then
success = delete_dir(path)
if not success then
return api.nvim_err_writeln("Could not remove directory: " .. path)
end
end
else
local success = loop.fs_unlink(path)
if not success then
return api.nvim_err_writeln("Could not remove file: " .. path)
end
clear_buffer(path)
end
vim.schedule(function()
events.fire_event(events.FILE_DELETED, path)
if callback then
callback(path)
end
end)
end
if noconfirm then
do_delete(true)
else
inputs.confirm(msg, do_delete)
end
end
M.delete_nodes = function(paths_to_delete, callback)
local msg = "Are you sure you want to delete " .. #paths_to_delete .. " items?"
inputs.confirm(msg, function(confirmed)
if not confirmed then
return
end
for _, path in ipairs(paths_to_delete) do
M.delete_node(path, nil, true)
end
if callback then
vim.schedule(function()
callback(paths_to_delete[#paths_to_delete])
end)
end
end)
end
-- Rename Node
M.rename_node = function(path, callback)
local parent_path, name = utils.split_path(path)
local msg = string.format('Enter new name for "%s":', name)
inputs.input(msg, name, function(new_name)
-- If cancelled
if not new_name or new_name == "" then
log.info("Operation canceled")
return
end
local destination = parent_path .. utils.path_separator .. new_name
-- If aleady exists
if loop.fs_stat(destination) then
log.warn(destination, " already exists")
return
end
local complete = vim.schedule_wrap(function()
rename_buffer(path, destination)
events.fire_event(events.FILE_RENAMED, {
source = path,
destination = destination,
})
if callback then
callback(path, destination)
end
log.info("Renamed " .. new_name .. " successfully")
end)
local function fs_rename()
loop.fs_rename(path, destination, function(err)
if err then
log.warn("Could not rename the files")
return
else
complete()
end
end)
end
local event_result = events.fire_event(events.BEFORE_FILE_RENAME, {
source = path,
destination = destination,
callback = fs_rename,
}) or {}
if event_result.handled then
return
end
fs_rename()
end)
end
return M