Problem: the codebase still used the upstream \`oil\` naming everywhere — URL schemes, the \`:Oil\` command, highlight groups, user events, module paths, filetypes, buffer/window variables, LuaCATS type annotations, vimdoc help tags, syntax groups, and internal identifiers. Solution: mechanical rename of every reference. URL schemes now use \`canola://\` (plus \`canola-ssh://\`, \`canola-s3://\`, \`canola-sss://\`, \`canola-trash://\`, \`canola-test://\`). The \`:Canola\` command replaces \`:Oil\`. All highlight groups, user events, augroups, namespaces, filetypes, require paths, type annotations, help tags, and identifiers follow suit. The \`upstream\` remote to \`stevearc/oil.nvim\` has been removed and the \`vim.g.oil\` deprecation shim dropped.
385 lines
9.8 KiB
Lua
385 lines
9.8 KiB
Lua
local log = require('canola.log')
|
|
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 mode? integer File mode in decimal (default 420 = 0644)
|
|
---@param cb fun(err: nil|string)
|
|
M.touch = function(path, mode, cb)
|
|
if type(mode) == 'function' then
|
|
cb = mode
|
|
mode = 420
|
|
end
|
|
uv.fs_open(path, 'a', mode or 420, function(err, fd)
|
|
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+)')
|
|
if not drive then
|
|
return path
|
|
end
|
|
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: canola.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 canola.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 err2 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
|
|
|
|
---Move the undofile for the file at src_path to dest_path
|
|
---@param src_path string
|
|
---@param dest_path string
|
|
---@param copy boolean
|
|
local move_undofile = vim.schedule_wrap(function(src_path, dest_path, copy)
|
|
local undofile = vim.fn.undofile(src_path)
|
|
uv.fs_stat(
|
|
undofile,
|
|
vim.schedule_wrap(function(stat_err)
|
|
if stat_err then
|
|
-- undofile doesn't exist
|
|
return
|
|
end
|
|
local dest_undofile = vim.fn.undofile(dest_path)
|
|
if copy then
|
|
uv.fs_copyfile(src_path, dest_path, function(err)
|
|
if err then
|
|
log.warn('Error copying undofile %s: %s', undofile, err)
|
|
end
|
|
end)
|
|
else
|
|
uv.fs_rename(undofile, dest_undofile, function(err)
|
|
if err then
|
|
log.warn('Error moving undofile %s: %s', undofile, err)
|
|
end
|
|
end)
|
|
end
|
|
end)
|
|
)
|
|
end)
|
|
|
|
---@param entry_type canola.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)
|
|
move_undofile(src_path, dest_path, true)
|
|
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 err2 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 canola.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
|
|
if entry_type ~= 'directory' then
|
|
move_undofile(src_path, dest_path, false)
|
|
end
|
|
cb()
|
|
end
|
|
end)
|
|
end
|
|
|
|
return M
|