pages/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/fs.lua

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