323 lines
7.9 KiB
Lua
Executable File
323 lines
7.9 KiB
Lua
Executable File
local api = vim.api
|
|
local uv = vim.loop
|
|
|
|
local config = require("dirbuf.config")
|
|
|
|
local M = {}
|
|
|
|
M.path_separator = package.config:sub(1, 1)
|
|
|
|
function M.is_hidden(fname)
|
|
return fname:sub(1, 1) == "."
|
|
end
|
|
|
|
function M.join_paths(...)
|
|
local string_builder = {}
|
|
for _, path in ipairs({ ... }) do
|
|
if path:sub(-1, -1) == M.path_separator then
|
|
path = path:sub(0, -2)
|
|
end
|
|
table.insert(string_builder, path)
|
|
end
|
|
return table.concat(string_builder, M.path_separator)
|
|
end
|
|
|
|
function M.is_directory(path)
|
|
return vim.fn.isdirectory(path) == 1
|
|
end
|
|
|
|
-- FTypes are taken from
|
|
-- https://github.com/tbastos/luv/blob/2fed9454ebb870548cef1081a1f8a3dd879c1e70/src/fs.c#L420-L430
|
|
--[[
|
|
local enum FType
|
|
"file"
|
|
"directory"
|
|
"link"
|
|
"fifo"
|
|
"socket"
|
|
"char"
|
|
"block"
|
|
end
|
|
local record FSEntry
|
|
fname: string
|
|
ftype: FType
|
|
path: string
|
|
end
|
|
--]]
|
|
|
|
M.FSEntry = {}
|
|
local FSEntry = M.FSEntry
|
|
|
|
function FSEntry.new(fname, parent, ftype)
|
|
return { fname = fname, path = M.join_paths(parent, fname), ftype = ftype }
|
|
end
|
|
|
|
function FSEntry.temp(ftype)
|
|
local temppath = vim.fn.tempname()
|
|
return {
|
|
-- XXX: This technically violates fname's assumption that it is alwaies a
|
|
-- simple name and not a path
|
|
fname = temppath,
|
|
path = temppath,
|
|
ftype = ftype,
|
|
}
|
|
end
|
|
|
|
function M.get_fs_entries(dir, show_hidden)
|
|
local fs_entries = {}
|
|
|
|
local handle, err, _ = uv.fs_scandir(dir)
|
|
if handle == nil then
|
|
return err
|
|
end
|
|
|
|
while true do
|
|
local fname, ftype = uv.fs_scandir_next(handle)
|
|
if fname == nil then
|
|
break
|
|
end
|
|
if show_hidden or not M.is_hidden(fname) then
|
|
table.insert(fs_entries, FSEntry.new(fname, dir, ftype))
|
|
end
|
|
end
|
|
table.sort(fs_entries, config.get("sort_order"))
|
|
|
|
return nil, fs_entries
|
|
end
|
|
|
|
M.plan = {}
|
|
M.actions = {}
|
|
|
|
local DEFAULT_FILE_MODE = tonumber("644", 8)
|
|
-- Directories have to be executable for you to chdir into them
|
|
local DEFAULT_DIR_MODE = tonumber("755", 8)
|
|
|
|
local function cp(src_path, dst_path, ftype)
|
|
if ftype == "directory" then
|
|
local ok, err, _ = uv.fs_mkdir(dst_path, DEFAULT_DIR_MODE)
|
|
if not ok then
|
|
return err
|
|
end
|
|
|
|
local handle = uv.fs_scandir(src_path)
|
|
while true do
|
|
local next_fname, next_ftype = uv.fs_scandir_next(handle)
|
|
if next_fname == nil then
|
|
break
|
|
end
|
|
err = cp(M.join_paths(src_path, next_fname), M.join_paths(dst_path, next_fname), next_ftype)
|
|
if err ~= nil then
|
|
return err
|
|
end
|
|
end
|
|
|
|
return nil
|
|
elseif ftype == "link" then
|
|
local src_points_to, err, _ = uv.fs_readlink(src_path)
|
|
if src_points_to == nil then
|
|
return err
|
|
end
|
|
local ok
|
|
ok, err, _ = uv.fs_symlink(src_points_to, dst_path)
|
|
if not ok then
|
|
return err
|
|
end
|
|
|
|
return nil
|
|
else
|
|
local ok, err, _ = uv.fs_copyfile(src_path, dst_path)
|
|
if not ok then
|
|
return err
|
|
end
|
|
return nil
|
|
end
|
|
end
|
|
|
|
local function rm(path, ftype)
|
|
if ftype == "directory" then
|
|
local handle = uv.fs_scandir(path)
|
|
while true do
|
|
local next_fname, next_ftype = uv.fs_scandir_next(handle)
|
|
if next_fname == nil then
|
|
break
|
|
end
|
|
local err = rm(M.join_paths(path, next_fname), next_ftype)
|
|
if err ~= nil then
|
|
return err
|
|
end
|
|
end
|
|
local ok, err, _ = uv.fs_rmdir(path)
|
|
if not ok then
|
|
return err
|
|
end
|
|
return nil
|
|
else
|
|
local ok, err, _ = uv.fs_unlink(path)
|
|
if not ok then
|
|
return err
|
|
end
|
|
return nil
|
|
end
|
|
end
|
|
|
|
local function mv(src_path, dst_path, ftype)
|
|
-- FIXME: This is a TOCTOU
|
|
if uv.fs_access(dst_path, "W") then
|
|
return string.format("'%s' already exists", dst_path)
|
|
end
|
|
local ok, err, err_type = uv.fs_rename(src_path, dst_path)
|
|
|
|
if not ok and err_type == "EXDEV" then
|
|
err = cp(src_path, dst_path, ftype)
|
|
if err ~= nil then
|
|
return err
|
|
end
|
|
err = rm(src_path, ftype)
|
|
if err ~= nil then
|
|
return err
|
|
end
|
|
elseif not ok then
|
|
return err
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function is_child_of(maybe_child, parent)
|
|
local exact_match = maybe_child == parent
|
|
local child_match = vim.startswith(maybe_child, parent .. M.path_separator)
|
|
return exact_match or child_match
|
|
end
|
|
|
|
-- `rename_loaded_buffers` finds all renamed buffers under `old_path` and
|
|
-- renames them to be under `new_path`.
|
|
local function rename_loaded_buffers(old_path, new_path)
|
|
for _, buf in ipairs(api.nvim_list_bufs()) do
|
|
if not api.nvim_buf_is_loaded(buf) then
|
|
goto continue
|
|
end
|
|
|
|
-- api.nvim_buf_get_name() returns absolute path so no post-processing
|
|
local buf_name = api.nvim_buf_get_name(buf)
|
|
if is_child_of(buf_name, old_path) then
|
|
api.nvim_buf_set_name(buf, new_path .. buf_name:sub(#old_path + 1))
|
|
|
|
-- We have to :write! normal files to avoid `E13: File exists (add ! to
|
|
-- override)` error when manually calling :write
|
|
if api.nvim_buf_get_option(buf, "buftype") == "" then
|
|
api.nvim_buf_call(buf, function()
|
|
vim.cmd("silent! write!")
|
|
end)
|
|
end
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
end
|
|
|
|
-- `delete_loaded_buffers` finds all deleted buffers under `path` and replaces
|
|
-- them with their alternate buffer, or a [No Name] buffer if its alternate
|
|
-- buffer doesn't exist.
|
|
local function delete_loaded_buffers(path)
|
|
for _, buf in ipairs(api.nvim_list_bufs()) do
|
|
if not api.nvim_buf_is_loaded(buf) then
|
|
goto continue
|
|
end
|
|
|
|
-- api.nvim_buf_get_name() returns absolute path so no post-processing
|
|
local buf_name = api.nvim_buf_get_name(buf)
|
|
if is_child_of(buf_name, path) then
|
|
for _, win in ipairs(vim.fn.win_findbuf(buf)) do
|
|
api.nvim_win_call(win, function()
|
|
local altbuf = vim.fn.bufnr("#")
|
|
if api.nvim_buf_is_valid(altbuf) then
|
|
api.nvim_win_set_buf(win, altbuf)
|
|
else
|
|
vim.cmd("enew!")
|
|
end
|
|
end)
|
|
end
|
|
api.nvim_buf_delete(buf, { force = true })
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
end
|
|
|
|
function M.plan.create(fs_entry)
|
|
return { type = "create", fs_entry = fs_entry }
|
|
end
|
|
|
|
function M.actions.create(args)
|
|
local fs_entry = args.fs_entry
|
|
|
|
-- FIXME: This is a TOCTOU
|
|
if uv.fs_access(fs_entry.path, "W") then
|
|
return string.format("'%s' already exists", fs_entry.ftype, fs_entry.path)
|
|
end
|
|
|
|
if fs_entry.ftype == "file" then
|
|
local fd, err = uv.fs_open(fs_entry.path, "w", DEFAULT_FILE_MODE)
|
|
if fd == nil then
|
|
return err
|
|
end
|
|
local ok
|
|
ok, err = uv.fs_close(fd)
|
|
if not ok then
|
|
return err
|
|
end
|
|
elseif fs_entry.ftype == "directory" then
|
|
local ok, err = uv.fs_mkdir(fs_entry.path, DEFAULT_DIR_MODE)
|
|
if not ok then
|
|
return err
|
|
end
|
|
else
|
|
return string.format("Cannot create %s", fs_entry.ftype)
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function M.plan.copy(src_fs_entry, dst_fs_entry)
|
|
return { type = "copy", src_fs_entry = src_fs_entry, dst_fs_entry = dst_fs_entry }
|
|
end
|
|
|
|
function M.actions.copy(args)
|
|
local src_fs_entry, dst_fs_entry = args.src_fs_entry, args.dst_fs_entry
|
|
-- planner ensures src and dst have same ftype
|
|
return cp(src_fs_entry.path, dst_fs_entry.path, src_fs_entry.ftype)
|
|
end
|
|
|
|
function M.plan.delete(fs_entry)
|
|
return { type = "delete", fs_entry = fs_entry }
|
|
end
|
|
|
|
function M.actions.delete(args)
|
|
local fs_entry = args.fs_entry
|
|
local err = rm(fs_entry.path, fs_entry.ftype)
|
|
if err ~= nil then
|
|
return string.format("Delete %s: %s", fs_entry.path, err)
|
|
end
|
|
|
|
delete_loaded_buffers(fs_entry.path)
|
|
return nil
|
|
end
|
|
|
|
function M.plan.move(src_fs_entry, dst_fs_entry)
|
|
return { type = "move", src_fs_entry = src_fs_entry, dst_fs_entry = dst_fs_entry }
|
|
end
|
|
|
|
function M.actions.move(args)
|
|
local src_fs_entry, dst_fs_entry = args.src_fs_entry, args.dst_fs_entry
|
|
-- planner ensures src and dst have same ftype
|
|
local err = mv(src_fs_entry.path, dst_fs_entry.path, src_fs_entry.ftype)
|
|
if err ~= nil then
|
|
return string.format("Move failed for %s -> %s: %s", src_fs_entry.path, dst_fs_entry.path, err)
|
|
end
|
|
|
|
rename_loaded_buffers(src_fs_entry.path, dst_fs_entry.path)
|
|
return nil
|
|
end
|
|
|
|
return M
|