canola.nvim/lua/oil/adapters/trash/mac.lua
Barrett Ruth 86f553cd0a
build: replace luacheck with selene, add nix devshell and pre-commit (#20)
* build: replace luacheck with selene

Problem: luacheck is unmaintained (last release 2018) and required
suppressing four warning classes to avoid false positives. It also
lacks first-class vim/neovim awareness.

Solution: switch to selene with std='vim' for vim-aware linting.
Replace the luacheck CI job with selene, update the Makefile lint
target, and delete .luacheckrc.

* build: add nix devshell and pre-commit hooks

Problem: oil.nvim had no reproducible dev environment. The .envrc
set up a Python venv for the now-removed docgen pipeline, and there
were no pre-commit hooks for local formatting checks.

Solution: add flake.nix with stylua, selene, and prettier in the
devshell. Replace the stale Python .envrc with 'use flake'. Add
.pre-commit-config.yaml with stylua and prettier hooks matching
other plugins in the repo collection.

* fix: format with stylua

* build(selene): configure lints and add inline suppressions

Problem: selene fails on 5 errors and 3 warnings from upstream code
patterns that are intentional (mixed tables in config API, unused
callback parameters, identical if branches for readability).

Solution: globally allow mixed_table and unused_variable (high volume,
inherent to the codebase design). Add inline selene:allow directives
for the 8 remaining issues: if_same_then_else (4), mismatched_arg_count
(1), empty_if (2), global_usage (1). Remove .envrc from tracking.

* build: switch typecheck action to mrcjkb/lua-typecheck-action

Problem: oil.nvim used stevearc/nvim-typecheck-action, which required
cloning the action repo locally for the Makefile lint target. All
other plugins in the collection use mrcjkb/lua-typecheck-action.

Solution: swap to mrcjkb/lua-typecheck-action@v0 for consistency.
Remove the nvim-typecheck-action git clone from the Makefile and
.gitignore. Drop LuaLS from the local lint target since it requires
a full language server install — CI handles it.
2026-02-21 23:52:27 -05:00

232 lines
7 KiB
Lua

local cache = require('oil.cache')
local config = require('oil.config')
local files = require('oil.adapters.files')
local fs = require('oil.fs')
local util = require('oil.util')
local uv = vim.uv or vim.loop
local M = {}
local function touch_dir(path)
uv.fs_mkdir(path, 448) -- 0700
end
---Gets the location of the home trash dir, creating it if necessary
---@return string
local function get_trash_dir()
local trash_dir = fs.join(assert(uv.os_homedir()), '.Trash')
touch_dir(trash_dir)
return trash_dir
end
---@param url string
---@param callback fun(url: string)
M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url)
assert(path)
callback(scheme .. '/')
end
---@param url string
---@param entry oil.Entry
---@param cb fun(path: string)
M.get_entry_path = function(url, entry, cb)
local trash_dir = get_trash_dir()
local path = fs.join(trash_dir, entry.name)
if entry.type == 'directory' then
path = 'oil://' .. path
end
cb(path)
end
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, cb)
cb = vim.schedule_wrap(cb)
local _, path = util.parse_url(url)
assert(path)
local trash_dir = get_trash_dir()
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(trash_dir, function(open_err, fd)
if open_err then
if open_err:match('^ENOENT: no such file or directory') then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse
-- and edit a not-yet-existing directory.
return cb()
else
return cb(open_err)
end
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
local internal_entries = {}
local poll = util.cb_collect(#entries, function(inner_err)
if inner_err then
cb(inner_err)
else
cb(nil, internal_entries, read_next)
end
end)
for _, entry in ipairs(entries) do
-- TODO: read .DS_Store and filter by original dir
local cache_entry = cache.create_entry(url, entry.name, entry.type)
table.insert(internal_entries, cache_entry)
poll()
end
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 bufnr integer
---@return boolean
M.is_modifiable = function(bufnr)
return true
end
---@param name string
---@return nil|oil.ColumnDefinition
M.get_column = function(name)
return nil
end
M.supported_cross_adapter_actions = { files = 'move' }
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == 'create' then
return string.format('CREATE %s', action.url)
elseif action.type == 'delete' then
return string.format(' PURGE %s', action.url)
elseif action.type == 'move' then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(' TRASH %s', short_path)
elseif dest_adapter.name == 'files' then
local _, path = util.parse_url(action.dest_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format('RESTORE %s', short_path)
else
return string.format(' %s %s -> %s', action.type:upper(), action.src_url, action.dest_url)
end
elseif action.type == 'copy' then
return string.format(' %s %s -> %s', action.type:upper(), action.src_url, action.dest_url)
else
error('Bad action type')
end
end
---@param action oil.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
local trash_dir = get_trash_dir()
if action.type == 'create' then
local _, path = util.parse_url(action.url)
assert(path)
path = trash_dir .. path
if action.entry_type == 'directory' then
uv.fs_mkdir(path, 493, function(err)
-- Ignore if the directory already exists
if not err or err:match('^EEXIST:') then
cb()
else
cb(err)
end
end) -- 0755
elseif action.entry_type == 'link' and action.link then
local flags = nil
local target = fs.posix_to_os_path(action.link)
---@diagnostic disable-next-line: param-type-mismatch
uv.fs_symlink(target, path, flags, cb)
else
fs.touch(path, config.new_file_mode, cb)
end
elseif action.type == 'delete' then
local _, path = util.parse_url(action.url)
assert(path)
local fullpath = trash_dir .. path
fs.recursive_delete(action.entry_type, fullpath, cb)
elseif action.type == 'move' or action.type == 'copy' then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
local _, src_path = util.parse_url(action.src_url)
local _, dest_path = util.parse_url(action.dest_url)
assert(src_path and dest_path)
if src_adapter.name == 'files' then
dest_path = trash_dir .. dest_path
elseif dest_adapter.name == 'files' then
src_path = trash_dir .. src_path
else
dest_path = trash_dir .. dest_path
src_path = trash_dir .. src_path
end
if action.type == 'move' then
fs.recursive_move(action.entry_type, src_path, dest_path, cb)
else
fs.recursive_copy(action.entry_type, src_path, dest_path, cb)
end
else
cb(string.format('Bad action type: %s', action.type))
end
end
---@param path string
---@param cb fun(err?: string)
M.delete_to_trash = function(path, cb)
local basename = vim.fs.basename(path)
local trash_dir = get_trash_dir()
local dest = fs.join(trash_dir, basename)
uv.fs_lstat(
path,
vim.schedule_wrap(function(stat_err, src_stat)
if stat_err then
return cb(stat_err)
end
assert(src_stat)
if uv.fs_lstat(dest) then
local date_str = vim.fn.strftime(' %Y-%m-%dT%H:%M:%S')
local name_pieces = vim.split(basename, '.', { plain = true })
if #name_pieces > 1 then
table.insert(name_pieces, #name_pieces - 1, date_str)
basename = table.concat(name_pieces)
else
basename = basename .. date_str
end
dest = fs.join(trash_dir, basename)
end
local stat_type = src_stat.type
fs.recursive_move(stat_type, path, dest, vim.schedule_wrap(cb))
end)
)
end
return M