canola.nvim/lua/oil/fs.lua
2024-05-13 20:02:11 -06:00

341 lines
8.6 KiB
Lua

local M = {}
local uv = vim.uv or vim.loop
---@type boolean
M.is_windows = uv.os_uname().version:match("Windows")
M.is_mac = uv.os_uname().sysname == "Darwin"
M.is_linux = not M.is_windows and not M.is_mac
---@type string
M.sep = M.is_windows and "\\" or "/"
---@param ... string
M.join = function(...)
return table.concat({ ... }, M.sep)
end
---Check if OS path is absolute
---@param dir string
---@return boolean
M.is_absolute = function(dir)
if M.is_windows then
return dir:match("^%a:\\")
else
return vim.startswith(dir, "/")
end
end
M.abspath = function(path)
if not M.is_absolute(path) then
path = vim.fn.fnamemodify(path, ":p")
end
return path
end
---@param path string
---@param cb fun(err: nil|string)
M.touch = function(path, cb)
uv.fs_open(path, "a", 420, function(err, fd) -- 0644
if err then
cb(err)
else
assert(fd)
uv.fs_close(fd, cb)
end
end)
end
--- Returns true if candidate is a subpath of root, or if they are the same path.
---@param root string
---@param candidate string
---@return boolean
M.is_subpath = function(root, candidate)
if candidate == "" then
return false
end
root = vim.fs.normalize(M.abspath(root))
-- Trim trailing "/" from the root
if root:find("/", -1) then
root = root:sub(1, -2)
end
candidate = vim.fs.normalize(M.abspath(candidate))
if M.is_windows then
root = root:lower()
candidate = candidate:lower()
end
if root == candidate then
return true
end
local prefix = candidate:sub(1, root:len())
if prefix ~= root then
return false
end
local candidate_starts_with_sep = candidate:find("/", root:len() + 1, true) == root:len() + 1
local root_ends_with_sep = root:find("/", root:len(), true) == root:len()
return candidate_starts_with_sep or root_ends_with_sep
end
---@param path string
---@return string
M.posix_to_os_path = function(path)
if M.is_windows then
if vim.startswith(path, "/") then
local drive = path:match("^/(%a+)")
local rem = path:sub(drive:len() + 2)
return string.format("%s:%s", drive, rem:gsub("/", "\\"))
else
local newpath = path:gsub("/", "\\")
return newpath
end
else
return path
end
end
---@param path string
---@return string
M.os_to_posix_path = function(path)
if M.is_windows then
if M.is_absolute(path) then
local drive, rem = path:match("^([^:]+):\\(.*)$")
return string.format("/%s/%s", drive:upper(), rem:gsub("\\", "/"))
else
local newpath = path:gsub("\\", "/")
return newpath
end
else
return path
end
end
local home_dir = assert(uv.os_homedir())
---@param path string
---@param relative_to? string Shorten relative to this path (default cwd)
---@return string
M.shorten_path = function(path, relative_to)
if not relative_to then
relative_to = vim.fn.getcwd()
end
local relpath
if M.is_subpath(relative_to, path) then
local idx = relative_to:len() + 1
-- Trim the dividing slash if it's not included in relative_to
if not vim.endswith(relative_to, "/") and not vim.endswith(relative_to, "\\") then
idx = idx + 1
end
relpath = path:sub(idx)
if relpath == "" then
relpath = "."
end
end
if M.is_subpath(home_dir, path) then
local homepath = "~" .. path:sub(home_dir:len() + 1)
if not relpath or homepath:len() < relpath:len() then
return homepath
end
end
return relpath or path
end
---@param dir string
---@param mode? integer
M.mkdirp = function(dir, mode)
mode = mode or 493
local mod = ""
local path = dir
while vim.fn.isdirectory(path) == 0 do
mod = mod .. ":h"
path = vim.fn.fnamemodify(dir, mod)
end
while mod ~= "" do
mod = mod:sub(3)
path = vim.fn.fnamemodify(dir, mod)
uv.fs_mkdir(path, mode)
end
end
---@param dir string
---@param cb fun(err: nil|string, entries: nil|{type: oil.EntryType, name: string})
M.listdir = function(dir, cb)
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(dir, function(open_err, fd)
if open_err then
return cb(open_err)
end
local read_next
read_next = function()
uv.fs_readdir(fd, function(err, entries)
if err then
uv.fs_closedir(fd, function()
cb(err)
end)
return
elseif entries then
---@diagnostic disable-next-line: param-type-mismatch
cb(nil, entries)
read_next()
else
uv.fs_closedir(fd, function(close_err)
if close_err then
cb(close_err)
else
cb()
end
end)
end
end)
end
read_next()
---@diagnostic disable-next-line: param-type-mismatch
end, 10000)
end
---@param entry_type oil.EntryType
---@param path string
---@param cb fun(err: nil|string)
M.recursive_delete = function(entry_type, path, cb)
if entry_type ~= "directory" then
return uv.fs_unlink(path, cb)
end
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(path, function(open_err, fd)
if open_err then
return cb(open_err)
end
local poll
poll = function(inner_cb)
uv.fs_readdir(fd, function(err, entries)
if err then
return inner_cb(err)
elseif entries then
local waiting = #entries
local complete
complete = function(err2)
if err then
complete = function() end
return inner_cb(err2)
end
waiting = waiting - 1
if waiting == 0 then
poll(inner_cb)
end
end
for _, entry in ipairs(entries) do
M.recursive_delete(entry.type, path .. M.sep .. entry.name, complete)
end
else
inner_cb()
end
end)
end
poll(function(err)
uv.fs_closedir(fd)
if err then
return cb(err)
end
uv.fs_rmdir(path, cb)
end)
---@diagnostic disable-next-line: param-type-mismatch
end, 10000)
end
---@param entry_type oil.EntryType
---@param src_path string
---@param dest_path string
---@param cb fun(err: nil|string)
M.recursive_copy = function(entry_type, src_path, dest_path, cb)
if entry_type == "link" then
uv.fs_readlink(src_path, function(link_err, link)
if link_err then
return cb(link_err)
end
assert(link)
uv.fs_symlink(link, dest_path, 0, cb)
end)
return
end
if entry_type ~= "directory" then
uv.fs_copyfile(src_path, dest_path, { excl = true }, cb)
return
end
uv.fs_stat(src_path, function(stat_err, src_stat)
if stat_err then
return cb(stat_err)
end
assert(src_stat)
uv.fs_mkdir(dest_path, src_stat.mode, function(mkdir_err)
if mkdir_err then
return cb(mkdir_err)
end
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(src_path, function(open_err, fd)
if open_err then
return cb(open_err)
end
local poll
poll = function(inner_cb)
uv.fs_readdir(fd, function(err, entries)
if err then
return inner_cb(err)
elseif entries then
local waiting = #entries
local complete
complete = function(err2)
if err then
complete = function() end
return inner_cb(err2)
end
waiting = waiting - 1
if waiting == 0 then
poll(inner_cb)
end
end
for _, entry in ipairs(entries) do
M.recursive_copy(
entry.type,
src_path .. M.sep .. entry.name,
dest_path .. M.sep .. entry.name,
complete
)
end
else
inner_cb()
end
end)
end
poll(cb)
---@diagnostic disable-next-line: param-type-mismatch
end, 10000)
end)
end)
end
---@param entry_type oil.EntryType
---@param src_path string
---@param dest_path string
---@param cb fun(err: nil|string)
M.recursive_move = function(entry_type, src_path, dest_path, cb)
uv.fs_rename(src_path, dest_path, function(err)
if err then
-- fs_rename fails for cross-partition or cross-device operations.
-- We then fall back to a copy + delete
M.recursive_copy(entry_type, src_path, dest_path, function(err2)
if err2 then
cb(err2)
else
M.recursive_delete(entry_type, src_path, cb)
end
end)
else
cb()
end
end)
end
return M