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.
This commit is contained in:
Barrett Ruth 2026-02-21 23:52:27 -05:00 committed by GitHub
parent df53b172a9
commit 86f553cd0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 2762 additions and 2649 deletions

View file

@ -1,12 +1,12 @@
local cache = require("oil.cache")
local columns = require("oil.columns")
local config = require("oil.config")
local constants = require("oil.constants")
local fs = require("oil.fs")
local git = require("oil.git")
local log = require("oil.log")
local permissions = require("oil.adapters.files.permissions")
local util = require("oil.util")
local cache = require('oil.cache')
local columns = require('oil.columns')
local config = require('oil.config')
local constants = require('oil.constants')
local fs = require('oil.fs')
local git = require('oil.git')
local log = require('oil.log')
local permissions = require('oil.adapters.files.permissions')
local util = require('oil.util')
local uv = vim.uv or vim.loop
local M = {}
@ -25,7 +25,7 @@ local function read_link_data(path, cb)
assert(link)
local stat_path = link
if not fs.is_absolute(link) then
stat_path = fs.join(vim.fn.fnamemodify(path, ":h"), link)
stat_path = fs.join(vim.fn.fnamemodify(path, ':h'), link)
end
uv.fs_stat(stat_path, function(stat_err, stat)
cb(nil, link, stat)
@ -43,7 +43,7 @@ end
---@return string
M.to_short_os_path = function(path, entry_type)
local shortpath = fs.shorten_path(fs.posix_to_os_path(path))
if entry_type == "directory" then
if entry_type == 'directory' then
shortpath = util.addslash(shortpath, true)
end
return shortpath
@ -61,13 +61,13 @@ file_columns.size = {
return columns.EMPTY
end
if stat.size >= 1e9 then
return string.format("%.1fG", stat.size / 1e9)
return string.format('%.1fG', stat.size / 1e9)
elseif stat.size >= 1e6 then
return string.format("%.1fM", stat.size / 1e6)
return string.format('%.1fM', stat.size / 1e6)
elseif stat.size >= 1e3 then
return string.format("%.1fk", stat.size / 1e3)
return string.format('%.1fk', stat.size / 1e3)
else
return string.format("%d", stat.size)
return string.format('%d', stat.size)
end
end,
@ -82,7 +82,7 @@ file_columns.size = {
end,
parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$")
return line:match('^(%d+%S*)%s+(.*)$')
end,
}
@ -120,7 +120,7 @@ if not fs.is_windows then
local _, path = util.parse_url(action.url)
assert(path)
return string.format(
"CHMOD %s %s",
'CHMOD %s %s',
permissions.mode_to_octal_str(action.value),
M.to_short_os_path(path, action.entry_type)
)
@ -147,10 +147,10 @@ end
local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function()
current_year = vim.fn.strftime("%Y")
current_year = vim.fn.strftime('%Y')
end)
for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
for _, time_key in ipairs({ 'ctime', 'mtime', 'atime', 'birthtime' }) do
file_columns[time_key] = {
require_stat = true,
@ -165,11 +165,11 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
if fmt then
ret = vim.fn.strftime(fmt, stat[time_key].sec)
else
local year = vim.fn.strftime("%Y", stat[time_key].sec)
local year = vim.fn.strftime('%Y', stat[time_key].sec)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", stat[time_key].sec)
ret = vim.fn.strftime('%b %d %Y', stat[time_key].sec)
else
ret = vim.fn.strftime("%b %d %H:%M", stat[time_key].sec)
ret = vim.fn.strftime('%b %d %H:%M', stat[time_key].sec)
end
end
return ret
@ -183,20 +183,20 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
-- and whitespace with a pattern that matches any amount of whitespace
-- e.g. "%b %d %Y" -> "%S+%s+%S+%s+%S+"
pattern = fmt
:gsub("%%.", "%%S+")
:gsub("%s+", "%%s+")
:gsub('%%.', '%%S+')
:gsub('%s+', '%%s+')
-- escape `()[]` because those are special characters in Lua patterns
:gsub(
"%(",
"%%("
'%(',
'%%('
)
:gsub("%)", "%%)")
:gsub("%[", "%%[")
:gsub("%]", "%%]")
:gsub('%)', '%%)')
:gsub('%[', '%%[')
:gsub('%]', '%%]')
else
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
pattern = '%S+%s+%d+%s+%d%d:?%d%d'
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
return line:match('^(' .. pattern .. ')%s+(.+)$')
end,
get_sort_value = function(entry)
@ -238,17 +238,17 @@ M.normalize_url = function(url, callback)
assert(path)
if fs.is_windows then
if path == "/" then
if path == '/' then
return callback(url)
else
local is_root_drive = path:match("^/%u$")
local is_root_drive = path:match('^/%u$')
if is_root_drive then
return callback(url .. "/")
return callback(url .. '/')
end
end
end
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p")
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p')
uv.fs_realpath(os_path, function(err, new_os_path)
local realpath
if fs.is_windows then
@ -264,8 +264,8 @@ M.normalize_url = function(url, callback)
vim.schedule_wrap(function(stat_err, stat)
local is_directory
if stat then
is_directory = stat.type == "directory"
elseif vim.endswith(realpath, "/") or (fs.is_windows and vim.endswith(realpath, "\\")) then
is_directory = stat.type == 'directory'
elseif vim.endswith(realpath, '/') or (fs.is_windows and vim.endswith(realpath, '\\')) then
is_directory = true
else
local filetype = vim.filetype.match({ filename = vim.fs.basename(realpath) })
@ -276,7 +276,7 @@ M.normalize_url = function(url, callback)
local norm_path = util.addslash(fs.os_to_posix_path(realpath))
callback(scheme .. norm_path)
else
callback(vim.fn.fnamemodify(realpath, ":."))
callback(vim.fn.fnamemodify(realpath, ':.'))
end
end)
)
@ -292,11 +292,11 @@ M.get_entry_path = function(url, entry, cb)
local scheme, path = util.parse_url(parent_url)
M.normalize_url(scheme .. path .. entry.name, cb)
else
if entry.type == "directory" then
if entry.type == 'directory' then
cb(url)
else
local _, path = util.parse_url(url)
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(assert(path)), ":p")
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(assert(path)), ':p')
cb(os_path)
end
end
@ -321,10 +321,10 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
end
-- Make sure we always get fs_stat info for links
if entry[FIELD_TYPE] == "link" then
if entry[FIELD_TYPE] == 'link' then
read_link_data(entry_path, function(link_err, link, link_stat)
if link_err then
log.warn("Error reading link data %s: %s", entry_path, link_err)
log.warn('Error reading link data %s: %s', entry_path, link_err)
return cb()
end
meta.link = link
@ -336,7 +336,7 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
-- The link is broken, so let's use the stat of the link itself
uv.fs_lstat(entry_path, function(stat_err, stat)
if stat_err then
log.warn("Error lstat link file %s: %s", entry_path, stat_err)
log.warn('Error lstat link file %s: %s', entry_path, stat_err)
return cb()
end
meta.stat = stat
@ -350,7 +350,7 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
elseif require_stat then
uv.fs_stat(entry_path, function(stat_err, stat)
if stat_err then
log.warn("Error stat file %s: %s", entry_path, stat_err)
log.warn('Error stat file %s: %s', entry_path, stat_err)
return cb()
end
assert(stat)
@ -368,11 +368,11 @@ end
if fs.is_windows then
local old_fetch_metadata = fetch_entry_metadata
fetch_entry_metadata = function(parent_dir, entry, require_stat, cb)
if entry[FIELD_TYPE] == "link" then
if entry[FIELD_TYPE] == 'link' then
local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME])
uv.fs_lstat(entry_path, function(stat_err, stat)
if stat_err then
log.warn("Error lstat link file %s: %s", entry_path, stat_err)
log.warn('Error lstat link file %s: %s', entry_path, stat_err)
return old_fetch_metadata(parent_dir, entry, require_stat, cb)
end
assert(stat)
@ -398,17 +398,17 @@ local function list_windows_drives(url, column_defs, cb)
local _, path = util.parse_url(url)
assert(path)
local require_stat = columns_require_stat(column_defs)
local stdout = ""
local jid = vim.fn.jobstart({ "wmic", "logicaldisk", "get", "name" }, {
local stdout = ''
local jid = vim.fn.jobstart({ 'wmic', 'logicaldisk', 'get', 'name' }, {
stdout_buffered = true,
on_stdout = function(_, data)
stdout = table.concat(data, "\n")
stdout = table.concat(data, '\n')
end,
on_exit = function(_, code)
if code ~= 0 then
return cb("Error listing windows devices")
return cb('Error listing windows devices')
end
local lines = vim.split(stdout, "\n", { plain = true, trimempty = true })
local lines = vim.split(stdout, '\n', { plain = true, trimempty = true })
-- Remove the "Name" header
table.remove(lines, 1)
local internal_entries = {}
@ -421,12 +421,12 @@ local function list_windows_drives(url, column_defs, cb)
end)
for _, disk in ipairs(lines) do
if disk:match("^%s*$") then
if disk:match('^%s*$') then
-- Skip empty line
complete_disk_cb()
else
disk = disk:gsub(":%s*$", "")
local cache_entry = cache.create_entry(url, disk, "directory")
disk = disk:gsub(':%s*$', '')
local cache_entry = cache.create_entry(url, disk, 'directory')
table.insert(internal_entries, cache_entry)
fetch_entry_metadata(path, cache_entry, require_stat, complete_disk_cb)
end
@ -434,7 +434,7 @@ local function list_windows_drives(url, column_defs, cb)
end,
})
if jid <= 0 then
cb("Could not list windows devices")
cb('Could not list windows devices')
end
end
@ -444,7 +444,7 @@ end
M.list = function(url, column_defs, cb)
local _, path = util.parse_url(url)
assert(path)
if fs.is_windows and path == "/" then
if fs.is_windows and path == '/' then
return list_windows_drives(url, column_defs, cb)
end
local dir = fs.posix_to_os_path(path)
@ -453,7 +453,7 @@ M.list = function(url, column_defs, cb)
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(dir, function(open_err, fd)
if open_err then
if open_err:match("^ENOENT: no such file or directory") 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()
@ -505,7 +505,7 @@ M.is_modifiable = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr)
local _, path = util.parse_url(bufname)
assert(path)
if fs.is_windows and path == "/" then
if fs.is_windows and path == '/' then
return false
end
local dir = fs.posix_to_os_path(path)
@ -515,30 +515,30 @@ M.is_modifiable = function(bufnr)
end
-- fs_access can return nil, force boolean return
return uv.fs_access(dir, "W") == true
return uv.fs_access(dir, 'W') == true
end
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == "create" then
if action.type == 'create' then
local _, path = util.parse_url(action.url)
assert(path)
local ret = string.format("CREATE %s", M.to_short_os_path(path, action.entry_type))
local ret = string.format('CREATE %s', M.to_short_os_path(path, action.entry_type))
if action.link then
ret = ret .. " -> " .. fs.posix_to_os_path(action.link)
ret = ret .. ' -> ' .. fs.posix_to_os_path(action.link)
end
return ret
elseif action.type == "delete" then
elseif action.type == 'delete' then
local _, path = util.parse_url(action.url)
assert(path)
local short_path = M.to_short_os_path(path, action.entry_type)
if config.delete_to_trash then
return string.format(" TRASH %s", short_path)
return string.format(' TRASH %s', short_path)
else
return string.format("DELETE %s", short_path)
return string.format('DELETE %s', short_path)
end
elseif action.type == "move" or action.type == "copy" then
elseif action.type == 'move' or action.type == 'copy' then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url)
@ -546,7 +546,7 @@ M.render_action = function(action)
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
return string.format(
" %s %s -> %s",
' %s %s -> %s',
action.type:upper(),
M.to_short_os_path(src_path, action.entry_type),
M.to_short_os_path(dest_path, action.entry_type)
@ -563,7 +563,7 @@ end
---@param action oil.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
if action.type == "create" then
if action.type == 'create' then
local _, path = util.parse_url(action.url)
assert(path)
path = fs.posix_to_os_path(path)
@ -579,16 +579,16 @@ M.perform_action = function(action, cb)
end)
end
if action.entry_type == "directory" then
if action.entry_type == 'directory' then
uv.fs_mkdir(path, config.new_dir_mode, function(err)
-- Ignore if the directory already exists
if not err or err:match("^EEXIST:") then
if not err or err:match('^EEXIST:') then
cb()
else
cb(err)
end
end)
elseif action.entry_type == "link" and action.link then
elseif action.entry_type == 'link' and action.link then
local flags = nil
local target = fs.posix_to_os_path(action.link)
if fs.is_windows then
@ -602,7 +602,7 @@ M.perform_action = function(action, cb)
else
fs.touch(path, config.new_file_mode, cb)
end
elseif action.type == "delete" then
elseif action.type == 'delete' then
local _, path = util.parse_url(action.url)
assert(path)
path = fs.posix_to_os_path(path)
@ -619,11 +619,11 @@ M.perform_action = function(action, cb)
end
if config.delete_to_trash then
require("oil.adapters.trash").delete_to_trash(path, cb)
require('oil.adapters.trash').delete_to_trash(path, cb)
else
fs.recursive_delete(action.entry_type, path, cb)
end
elseif action.type == "move" then
elseif action.type == 'move' then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url)
@ -641,7 +641,7 @@ M.perform_action = function(action, cb)
-- We should never hit this because we don't implement supported_cross_adapter_actions
cb("files adapter doesn't support cross-adapter move")
end
elseif action.type == "copy" then
elseif action.type == 'copy' then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url)
@ -656,7 +656,7 @@ M.perform_action = function(action, cb)
cb("files adapter doesn't support cross-adapter copy")
end
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end

View file

@ -4,7 +4,7 @@ local M = {}
---@param num integer
---@return string
local function perm_to_str(exe_modifier, num)
local str = (bit.band(num, 4) ~= 0 and "r" or "-") .. (bit.band(num, 2) ~= 0 and "w" or "-")
local str = (bit.band(num, 4) ~= 0 and 'r' or '-') .. (bit.band(num, 2) ~= 0 and 'w' or '-')
if exe_modifier then
if bit.band(num, 1) ~= 0 then
return str .. exe_modifier
@ -12,7 +12,7 @@ local function perm_to_str(exe_modifier, num)
return str .. exe_modifier:upper()
end
else
return str .. (bit.band(num, 1) ~= 0 and "x" or "-")
return str .. (bit.band(num, 1) ~= 0 and 'x' or '-')
end
end
@ -20,9 +20,9 @@ end
---@return string
M.mode_to_str = function(mode)
local extra = bit.rshift(mode, 9)
return perm_to_str(bit.band(extra, 4) ~= 0 and "s", bit.rshift(mode, 6))
.. perm_to_str(bit.band(extra, 2) ~= 0 and "s", bit.rshift(mode, 3))
.. perm_to_str(bit.band(extra, 1) ~= 0 and "t", mode)
return perm_to_str(bit.band(extra, 4) ~= 0 and 's', bit.rshift(mode, 6))
.. perm_to_str(bit.band(extra, 2) ~= 0 and 's', bit.rshift(mode, 3))
.. perm_to_str(bit.band(extra, 1) ~= 0 and 't', mode)
end
---@param mode integer
@ -38,25 +38,25 @@ end
---@param str string String of 3 characters
---@return nil|integer
local function str_to_mode(str)
local r, w, x = unpack(vim.split(str, "", {}))
local r, w, x = unpack(vim.split(str, '', {}))
local mode = 0
if r == "r" then
if r == 'r' then
mode = bit.bor(mode, 4)
elseif r ~= "-" then
elseif r ~= '-' then
return nil
end
if w == "w" then
if w == 'w' then
mode = bit.bor(mode, 2)
elseif w ~= "-" then
elseif w ~= '-' then
return nil
end
-- t means sticky and executable
-- T means sticky, not executable
-- s means setuid/setgid and executable
-- S means setuid/setgid and not executable
if x == "x" or x == "t" or x == "s" then
if x == 'x' or x == 't' or x == 's' then
mode = bit.bor(mode, 1)
elseif x ~= "-" and x ~= "T" and x ~= "S" then
elseif x ~= '-' and x ~= 'T' and x ~= 'S' then
return nil
end
return mode
@ -67,13 +67,13 @@ end
local function parse_extra_bits(perm)
perm = perm:lower()
local mode = 0
if perm:sub(3, 3) == "s" then
if perm:sub(3, 3) == 's' then
mode = bit.bor(mode, 4)
end
if perm:sub(6, 6) == "s" then
if perm:sub(6, 6) == 's' then
mode = bit.bor(mode, 2)
end
if perm:sub(9, 9) == "t" then
if perm:sub(9, 9) == 't' then
mode = bit.bor(mode, 1)
end
return mode
@ -83,7 +83,7 @@ end
---@return nil|integer
---@return nil|string
M.parse = function(line)
local strval, rem = line:match("^([r%-][w%-][xsS%-][r%-][w%-][xsS%-][r%-][w%-][xtT%-])%s*(.*)$")
local strval, rem = line:match('^([r%-][w%-][xsS%-][r%-][w%-][xsS%-][r%-][w%-][xtT%-])%s*(.*)$')
if not strval then
return
end

View file

@ -1,11 +1,11 @@
local config = require("oil.config")
local constants = require("oil.constants")
local files = require("oil.adapters.files")
local fs = require("oil.fs")
local loading = require("oil.loading")
local pathutil = require("oil.pathutil")
local s3fs = require("oil.adapters.s3.s3fs")
local util = require("oil.util")
local config = require('oil.config')
local constants = require('oil.constants')
local files = require('oil.adapters.files')
local fs = require('oil.fs')
local loading = require('oil.loading')
local pathutil = require('oil.pathutil')
local s3fs = require('oil.adapters.s3.s3fs')
local util = require('oil.util')
local M = {}
local FIELD_META = constants.FIELD_META
@ -21,11 +21,11 @@ M.parse_url = function(oil_url)
local scheme, url = util.parse_url(oil_url)
assert(scheme and url, string.format("Malformed input url '%s'", oil_url))
local ret = { scheme = scheme }
local bucket, path = url:match("^([^/]+)/?(.*)$")
local bucket, path = url:match('^([^/]+)/?(.*)$')
ret.bucket = bucket
ret.path = path ~= "" and path or nil
ret.path = path ~= '' and path or nil
if not ret.bucket and ret.path then
error(string.format("Parsing error for s3 url: %s", oil_url))
error(string.format('Parsing error for s3 url: %s', oil_url))
end
---@cast ret oil.s3Url
return ret
@ -36,43 +36,43 @@ end
local function url_to_str(url)
local pieces = { url.scheme }
if url.bucket then
assert(url.bucket ~= "")
assert(url.bucket ~= '')
table.insert(pieces, url.bucket)
table.insert(pieces, "/")
table.insert(pieces, '/')
end
if url.path then
assert(url.path ~= "")
assert(url.path ~= '')
table.insert(pieces, url.path)
end
return table.concat(pieces, "")
return table.concat(pieces, '')
end
---@param url oil.s3Url
---@param is_folder boolean
---@return string
local function url_to_s3(url, is_folder)
local pieces = { "s3://" }
local pieces = { 's3://' }
if url.bucket then
assert(url.bucket ~= "")
assert(url.bucket ~= '')
table.insert(pieces, url.bucket)
table.insert(pieces, "/")
table.insert(pieces, '/')
end
if url.path then
assert(url.path ~= "")
assert(url.path ~= '')
table.insert(pieces, url.path)
if is_folder and not vim.endswith(url.path, "/") then
table.insert(pieces, "/")
if is_folder and not vim.endswith(url.path, '/') then
table.insert(pieces, '/')
end
end
return table.concat(pieces, "")
return table.concat(pieces, '')
end
---@param url oil.s3Url
---@return boolean
local function is_bucket(url)
assert(url.bucket and url.bucket ~= "")
assert(url.bucket and url.bucket ~= '')
if url.path then
assert(url.path ~= "")
assert(url.path ~= '')
return false
end
return true
@ -83,20 +83,20 @@ s3_columns.size = {
render = function(entry, conf)
local meta = entry[FIELD_META]
if not meta or not meta.size then
return ""
return ''
elseif meta.size >= 1e9 then
return string.format("%.1fG", meta.size / 1e9)
return string.format('%.1fG', meta.size / 1e9)
elseif meta.size >= 1e6 then
return string.format("%.1fM", meta.size / 1e6)
return string.format('%.1fM', meta.size / 1e6)
elseif meta.size >= 1e3 then
return string.format("%.1fk", meta.size / 1e3)
return string.format('%.1fk', meta.size / 1e3)
else
return string.format("%d", meta.size)
return string.format('%d', meta.size)
end
end,
parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$")
return line:match('^(%d+%S*)%s+(.*)$')
end,
get_sort_value = function(entry)
@ -113,21 +113,21 @@ s3_columns.birthtime = {
render = function(entry, conf)
local meta = entry[FIELD_META]
if not meta or not meta.date then
return ""
return ''
else
return meta.date
end
end,
parse = function(line, conf)
return line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$")
return line:match('^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$')
end,
get_sort_value = function(entry)
local meta = entry[FIELD_META]
if meta and meta.date then
local year, month, day, hour, min, sec =
meta.date:match("^(%d+)%-(%d+)%-(%d+)%s(%d+):(%d+):(%d+)$")
meta.date:match('^(%d+)%-(%d+)%-(%d+)%s(%d+):(%d+):(%d+)$')
local time =
os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
return time
@ -148,9 +148,9 @@ end
M.get_parent = function(bufname)
local res = M.parse_url(bufname)
if res.path then
assert(res.path ~= "")
assert(res.path ~= '')
local path = pathutil.parent(res.path)
res.path = path ~= "" and path or nil
res.path = path ~= '' and path or nil
else
res.bucket = nil
end
@ -168,8 +168,8 @@ end
---@param column_defs string[]
---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, callback)
if vim.fn.executable("aws") ~= 1 then
callback("`aws` is not executable. Can you run `aws s3 ls`?")
if vim.fn.executable('aws') ~= 1 then
callback('`aws` is not executable. Can you run `aws s3 ls`?')
return
end
@ -187,16 +187,16 @@ end
---@param action oil.Action
---@return string
M.render_action = function(action)
local is_folder = action.entry_type == "directory"
if action.type == "create" then
local is_folder = action.entry_type == 'directory'
if action.type == 'create' then
local res = M.parse_url(action.url)
local extra = is_bucket(res) and "BUCKET " or ""
return string.format("CREATE %s%s", extra, url_to_s3(res, is_folder))
elseif action.type == "delete" then
local extra = is_bucket(res) and 'BUCKET ' or ''
return string.format('CREATE %s%s', extra, url_to_s3(res, is_folder))
elseif action.type == 'delete' then
local res = M.parse_url(action.url)
local extra = is_bucket(res) and "BUCKET " or ""
return string.format("DELETE %s%s", extra, url_to_s3(res, is_folder))
elseif action.type == "move" or action.type == "copy" then
local extra = is_bucket(res) and 'BUCKET ' or ''
return string.format('DELETE %s%s', extra, url_to_s3(res, is_folder))
elseif action.type == 'move' or action.type == 'copy' then
local src = action.src_url
local dest = action.dest_url
if config.get_adapter_by_scheme(src) ~= M then
@ -210,7 +210,7 @@ M.render_action = function(action)
dest = files.to_short_os_path(path, action.entry_type)
src = url_to_s3(M.parse_url(src), is_folder)
end
return string.format(" %s %s -> %s", action.type:upper(), src, dest)
return string.format(' %s %s -> %s', action.type:upper(), src, dest)
else
error(string.format("Bad action type: '%s'", action.type))
end
@ -219,30 +219,30 @@ end
---@param action oil.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
local is_folder = action.entry_type == "directory"
if action.type == "create" then
local is_folder = action.entry_type == 'directory'
if action.type == 'create' then
local res = M.parse_url(action.url)
local bucket = is_bucket(res)
if action.entry_type == "directory" and bucket then
if action.entry_type == 'directory' and bucket then
s3fs.mb(url_to_s3(res, true), cb)
elseif action.entry_type == "directory" or action.entry_type == "file" then
elseif action.entry_type == 'directory' or action.entry_type == 'file' then
s3fs.touch(url_to_s3(res, is_folder), cb)
else
cb(string.format("Bad entry type on s3 create action: %s", action.entry_type))
cb(string.format('Bad entry type on s3 create action: %s', action.entry_type))
end
elseif action.type == "delete" then
elseif action.type == 'delete' then
local res = M.parse_url(action.url)
local bucket = is_bucket(res)
if action.entry_type == "directory" and bucket then
if action.entry_type == 'directory' and bucket then
s3fs.rb(url_to_s3(res, true), cb)
elseif action.entry_type == "directory" or action.entry_type == "file" then
elseif action.entry_type == 'directory' or action.entry_type == 'file' then
s3fs.rm(url_to_s3(res, is_folder), is_folder, cb)
else
cb(string.format("Bad entry type on s3 delete action: %s", action.entry_type))
cb(string.format('Bad entry type on s3 delete action: %s', action.entry_type))
end
elseif action.type == "move" then
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
@ -250,7 +250,7 @@ M.perform_action = function(action, cb)
then
cb(
string.format(
"We should never attempt to move from the %s adapter to the %s adapter.",
'We should never attempt to move from the %s adapter to the %s adapter.',
src_adapter.name,
dest_adapter.name
)
@ -276,7 +276,7 @@ M.perform_action = function(action, cb)
assert(dest)
s3fs.mv(src, dest, is_folder, cb)
elseif action.type == "copy" then
elseif 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))
if
@ -284,7 +284,7 @@ M.perform_action = function(action, cb)
then
cb(
string.format(
"We should never attempt to copy from the %s adapter to the %s adapter.",
'We should never attempt to copy from the %s adapter to the %s adapter.',
src_adapter.name,
dest_adapter.name
)
@ -311,11 +311,11 @@ M.perform_action = function(action, cb)
s3fs.cp(src, dest, is_folder, cb)
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end
M.supported_cross_adapter_actions = { files = "move" }
M.supported_cross_adapter_actions = { files = 'move' }
---@param bufnr integer
M.read_file = function(bufnr)
@ -323,11 +323,11 @@ M.read_file = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr)
local url = M.parse_url(bufname)
local basename = pathutil.basename(bufname)
local cache_dir = vim.fn.stdpath("cache")
assert(type(cache_dir) == "string")
local tmpdir = fs.join(cache_dir, "oil")
local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, 'oil')
fs.mkdirp(tmpdir)
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXX"))
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 's3_XXXXXX'))
if fd then
vim.loop.fs_close(fd)
end
@ -336,9 +336,9 @@ M.read_file = function(bufnr)
s3fs.cp(url_to_s3(url, false), tmpfile, false, function(err)
loading.set_loading(bufnr, false)
vim.bo[bufnr].modifiable = true
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufReadPre', bufname }, mods = { silent = true } })
if err then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, "\n"))
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, '\n'))
else
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {})
vim.api.nvim_buf_call(bufnr, function()
@ -352,7 +352,7 @@ M.read_file = function(bufnr)
if filetype then
vim.bo[bufnr].filetype = filetype
end
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufReadPost', bufname }, mods = { silent = true } })
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
end)
end
@ -361,14 +361,14 @@ end
M.write_file = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr)
local url = M.parse_url(bufname)
local cache_dir = vim.fn.stdpath("cache")
assert(type(cache_dir) == "string")
local tmpdir = fs.join(cache_dir, "oil")
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXXXX"))
local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, 'oil')
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 's3_XXXXXXXX'))
if fd then
vim.loop.fs_close(fd)
end
vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufWritePre', bufname }, mods = { silent = true } })
vim.bo[bufnr].modifiable = false
vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } })
local tmp_bufnr = vim.fn.bufadd(tmpfile)
@ -376,10 +376,10 @@ M.write_file = function(bufnr)
s3fs.cp(tmpfile, url_to_s3(url, false), false, function(err)
vim.bo[bufnr].modifiable = true
if err then
vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR)
vim.notify(string.format('Error writing file: %s', err), vim.log.levels.ERROR)
else
vim.bo[bufnr].modified = false
vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufWritePost', bufname }, mods = { silent = true } })
end
vim.loop.fs_unlink(tmpfile)
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })

View file

@ -1,8 +1,8 @@
local cache = require("oil.cache")
local config = require("oil.config")
local constants = require("oil.constants")
local shell = require("oil.shell")
local util = require("oil.util")
local cache = require('oil.cache')
local config = require('oil.config')
local constants = require('oil.constants')
local shell = require('oil.shell')
local util = require('oil.util')
local M = {}
@ -13,11 +13,11 @@ local FIELD_META = constants.FIELD_META
---@return oil.EntryType
---@return table Metadata for entry
local function parse_ls_line_bucket(line)
local date, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$")
local date, name = line:match('^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$')
if not date or not name then
error(string.format("Could not parse '%s'", line))
end
local type = "directory"
local type = 'directory'
local meta = { date = date }
return name, type, meta
end
@ -27,18 +27,18 @@ end
---@return oil.EntryType
---@return table Metadata for entry
local function parse_ls_line_file(line)
local name = line:match("^%s+PRE%s+(.*)/$")
local type = "directory"
local name = line:match('^%s+PRE%s+(.*)/$')
local type = 'directory'
local meta = {}
if name then
return name, type, meta
end
local date, size
date, size, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(%d+)%s+(.*)$")
date, size, name = line:match('^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(%d+)%s+(.*)$')
if not name then
error(string.format("Could not parse '%s'", line))
end
type = "file"
type = 'file'
meta = { date = date, size = tonumber(size) }
return name, type, meta
end
@ -46,7 +46,7 @@ end
---@param cmd string[] cmd and flags
---@return string[] Shell command to run
local function create_s3_command(cmd)
local full_cmd = vim.list_extend({ "aws", "s3" }, cmd)
local full_cmd = vim.list_extend({ 'aws', 's3' }, cmd)
return vim.list_extend(full_cmd, config.extra_s3_args)
end
@ -54,7 +54,7 @@ end
---@param path string
---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
function M.list_dir(url, path, callback)
local cmd = create_s3_command({ "ls", path, "--color=off", "--no-cli-pager" })
local cmd = create_s3_command({ 'ls', path, '--color=off', '--no-cli-pager' })
shell.run(cmd, function(err, lines)
if err then
return callback(err)
@ -63,13 +63,13 @@ function M.list_dir(url, path, callback)
local cache_entries = {}
local url_path, _
_, url_path = util.parse_url(url)
local is_top_level = url_path == nil or url_path:match("/") == nil
local is_top_level = url_path == nil or url_path:match('/') == nil
local parse_ls_line = is_top_level and parse_ls_line_bucket or parse_ls_line_file
for _, line in ipairs(lines) do
if line ~= "" then
if line ~= '' then
local name, type, meta = parse_ls_line(line)
-- in s3 '-' can be used to create an "empty folder"
if name ~= "-" then
if name ~= '-' then
local cache_entry = cache.create_entry(url, name, type)
table.insert(cache_entries, cache_entry)
cache_entry[FIELD_META] = meta
@ -85,8 +85,8 @@ end
---@param callback fun(err: nil|string)
function M.touch(path, callback)
-- here "-" means that we copy from stdin
local cmd = create_s3_command({ "cp", "-", path })
shell.run(cmd, { stdin = "null" }, callback)
local cmd = create_s3_command({ 'cp', '-', path })
shell.run(cmd, { stdin = 'null' }, callback)
end
--- Remove files
@ -94,9 +94,9 @@ end
---@param is_folder boolean
---@param callback fun(err: nil|string)
function M.rm(path, is_folder, callback)
local main_cmd = { "rm", path }
local main_cmd = { 'rm', path }
if is_folder then
table.insert(main_cmd, "--recursive")
table.insert(main_cmd, '--recursive')
end
local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback)
@ -106,7 +106,7 @@ end
---@param bucket string
---@param callback fun(err: nil|string)
function M.rb(bucket, callback)
local cmd = create_s3_command({ "rb", bucket })
local cmd = create_s3_command({ 'rb', bucket })
shell.run(cmd, callback)
end
@ -114,7 +114,7 @@ end
---@param bucket string
---@param callback fun(err: nil|string)
function M.mb(bucket, callback)
local cmd = create_s3_command({ "mb", bucket })
local cmd = create_s3_command({ 'mb', bucket })
shell.run(cmd, callback)
end
@ -124,9 +124,9 @@ end
---@param is_folder boolean
---@param callback fun(err: nil|string)
function M.mv(src, dest, is_folder, callback)
local main_cmd = { "mv", src, dest }
local main_cmd = { 'mv', src, dest }
if is_folder then
table.insert(main_cmd, "--recursive")
table.insert(main_cmd, '--recursive')
end
local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback)
@ -138,9 +138,9 @@ end
---@param is_folder boolean
---@param callback fun(err: nil|string)
function M.cp(src, dest, is_folder, callback)
local main_cmd = { "cp", src, dest }
local main_cmd = { 'cp', src, dest }
if is_folder then
table.insert(main_cmd, "--recursive")
table.insert(main_cmd, '--recursive')
end
local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback)

View file

@ -1,13 +1,13 @@
local config = require("oil.config")
local constants = require("oil.constants")
local files = require("oil.adapters.files")
local fs = require("oil.fs")
local loading = require("oil.loading")
local pathutil = require("oil.pathutil")
local permissions = require("oil.adapters.files.permissions")
local shell = require("oil.shell")
local sshfs = require("oil.adapters.ssh.sshfs")
local util = require("oil.util")
local config = require('oil.config')
local constants = require('oil.constants')
local files = require('oil.adapters.files')
local fs = require('oil.fs')
local loading = require('oil.loading')
local pathutil = require('oil.pathutil')
local permissions = require('oil.adapters.files.permissions')
local shell = require('oil.shell')
local sshfs = require('oil.adapters.ssh.sshfs')
local util = require('oil.util')
local M = {}
local FIELD_NAME = constants.FIELD_NAME
@ -22,7 +22,7 @@ local FIELD_META = constants.FIELD_META
---@param args string[]
local function scp(args, ...)
local cmd = vim.list_extend({ "scp", "-C" }, config.extra_scp_args)
local cmd = vim.list_extend({ 'scp', '-C' }, config.extra_scp_args)
vim.list_extend(cmd, args)
shell.run(cmd, ...)
end
@ -33,21 +33,21 @@ M.parse_url = function(oil_url)
local scheme, url = util.parse_url(oil_url)
assert(scheme and url, string.format("Malformed input url '%s'", oil_url))
local ret = { scheme = scheme }
local username, rem = url:match("^([^@%s]+)@(.*)$")
local username, rem = url:match('^([^@%s]+)@(.*)$')
ret.user = username
url = rem or url
local host, port, path = url:match("^([^:]+):(%d+)/(.*)$")
local host, port, path = url:match('^([^:]+):(%d+)/(.*)$')
if host then
ret.host = host
ret.port = tonumber(port)
ret.path = path
else
host, path = url:match("^([^/]+)/(.*)$")
host, path = url:match('^([^/]+)/(.*)$')
ret.host = host
ret.path = path
end
if not ret.host or not ret.path then
error(string.format("Malformed SSH url: %s", oil_url))
error(string.format('Malformed SSH url: %s', oil_url))
end
---@cast ret oil.sshUrl
@ -60,33 +60,33 @@ local function url_to_str(url)
local pieces = { url.scheme }
if url.user then
table.insert(pieces, url.user)
table.insert(pieces, "@")
table.insert(pieces, '@')
end
table.insert(pieces, url.host)
if url.port then
table.insert(pieces, string.format(":%d", url.port))
table.insert(pieces, string.format(':%d', url.port))
end
table.insert(pieces, "/")
table.insert(pieces, '/')
table.insert(pieces, url.path)
return table.concat(pieces, "")
return table.concat(pieces, '')
end
---@param url oil.sshUrl
---@return string
local function url_to_scp(url)
local pieces = { "scp://" }
local pieces = { 'scp://' }
if url.user then
table.insert(pieces, url.user)
table.insert(pieces, "@")
table.insert(pieces, '@')
end
table.insert(pieces, url.host)
if url.port then
table.insert(pieces, string.format(":%d", url.port))
table.insert(pieces, string.format(':%d', url.port))
end
table.insert(pieces, "/")
table.insert(pieces, '/')
local escaped_path = util.url_escape(url.path)
table.insert(pieces, escaped_path)
return table.concat(pieces, "")
return table.concat(pieces, '')
end
---@param url1 oil.sshUrl
@ -103,7 +103,7 @@ local _connections = {}
local function get_connection(url, allow_retry)
local res = M.parse_url(url)
res.scheme = config.adapter_to_scheme.ssh
res.path = ""
res.path = ''
local key = url_to_str(res)
local conn = _connections[key]
if not conn or (allow_retry and conn:get_connection_error()) then
@ -137,7 +137,7 @@ ssh_columns.permissions = {
end,
render_action = function(action)
return string.format("CHMOD %s %s", permissions.mode_to_octal_str(action.value), action.url)
return string.format('CHMOD %s %s', permissions.mode_to_octal_str(action.value), action.url)
end,
perform_action = function(action, callback)
@ -151,20 +151,20 @@ ssh_columns.size = {
render = function(entry, conf)
local meta = entry[FIELD_META]
if not meta or not meta.size then
return ""
return ''
elseif meta.size >= 1e9 then
return string.format("%.1fG", meta.size / 1e9)
return string.format('%.1fG', meta.size / 1e9)
elseif meta.size >= 1e6 then
return string.format("%.1fM", meta.size / 1e6)
return string.format('%.1fM', meta.size / 1e6)
elseif meta.size >= 1e3 then
return string.format("%.1fk", meta.size / 1e3)
return string.format('%.1fk', meta.size / 1e3)
else
return string.format("%d", meta.size)
return string.format('%d', meta.size)
end
end,
parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$")
return line:match('^(%d+%S*)%s+(.*)$')
end,
get_sort_value = function(entry)
@ -206,13 +206,13 @@ M.normalize_url = function(url, callback)
local conn = get_connection(url, true)
local path = res.path
if path == "" then
path = "."
if path == '' then
path = '.'
end
conn:realpath(path, function(err, abspath)
if err then
vim.notify(string.format("Error normalizing url %s: %s", url, err), vim.log.levels.WARN)
vim.notify(string.format('Error normalizing url %s: %s', url, err), vim.log.levels.WARN)
callback(url)
else
res.path = abspath
@ -259,15 +259,15 @@ end
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == "create" then
local ret = string.format("CREATE %s", action.url)
if action.type == 'create' then
local ret = string.format('CREATE %s', action.url)
if action.link then
ret = ret .. " -> " .. action.link
ret = ret .. ' -> ' .. action.link
end
return ret
elseif action.type == "delete" then
return string.format("DELETE %s", action.url)
elseif action.type == "move" or action.type == "copy" then
elseif action.type == 'delete' then
return string.format('DELETE %s', action.url)
elseif action.type == 'move' or action.type == 'copy' then
local src = action.src_url
local dest = action.dest_url
if config.get_adapter_by_scheme(src) == M then
@ -279,7 +279,7 @@ M.render_action = function(action)
assert(path)
src = files.to_short_os_path(path, action.entry_type)
end
return string.format(" %s %s -> %s", action.type:upper(), src, dest)
return string.format(' %s %s -> %s', action.type:upper(), src, dest)
else
error(string.format("Bad action type: '%s'", action.type))
end
@ -288,21 +288,21 @@ end
---@param action oil.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
if action.type == "create" then
if action.type == 'create' then
local res = M.parse_url(action.url)
local conn = get_connection(action.url)
if action.entry_type == "directory" then
if action.entry_type == 'directory' then
conn:mkdir(res.path, cb)
elseif action.entry_type == "link" and action.link then
elseif action.entry_type == 'link' and action.link then
conn:mklink(res.path, action.link, cb)
else
conn:touch(res.path, cb)
end
elseif action.type == "delete" then
elseif action.type == 'delete' then
local res = M.parse_url(action.url)
local conn = get_connection(action.url)
conn:rm(res.path, cb)
elseif action.type == "move" then
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 == M and dest_adapter == M then
@ -311,7 +311,7 @@ M.perform_action = function(action, cb)
local src_conn = get_connection(action.src_url)
local dest_conn = get_connection(action.dest_url)
if src_conn ~= dest_conn then
scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err)
scp({ '-r', url_to_scp(src_res), url_to_scp(dest_res) }, function(err)
if err then
return cb(err)
end
@ -321,16 +321,16 @@ M.perform_action = function(action, cb)
src_conn:mv(src_res.path, dest_res.path, cb)
end
else
cb("We should never attempt to move across adapters")
cb('We should never attempt to move across adapters')
end
elseif action.type == "copy" then
elseif 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))
if src_adapter == M and dest_adapter == M then
local src_res = M.parse_url(action.src_url)
local dest_res = M.parse_url(action.dest_url)
if not url_hosts_equal(src_res, dest_res) then
scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb)
scp({ '-r', url_to_scp(src_res), url_to_scp(dest_res) }, cb)
else
local src_conn = get_connection(action.src_url)
src_conn:cp(src_res.path, dest_res.path, cb)
@ -349,14 +349,14 @@ M.perform_action = function(action, cb)
src_arg = fs.posix_to_os_path(path)
dest_arg = url_to_scp(M.parse_url(action.dest_url))
end
scp({ "-r", src_arg, dest_arg }, cb)
scp({ '-r', src_arg, dest_arg }, cb)
end
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end
M.supported_cross_adapter_actions = { files = "copy" }
M.supported_cross_adapter_actions = { files = 'copy' }
---@param bufnr integer
M.read_file = function(bufnr)
@ -365,11 +365,11 @@ M.read_file = function(bufnr)
local url = M.parse_url(bufname)
local scp_url = url_to_scp(url)
local basename = pathutil.basename(bufname)
local cache_dir = vim.fn.stdpath("cache")
assert(type(cache_dir) == "string")
local tmpdir = fs.join(cache_dir, "oil")
local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, 'oil')
fs.mkdirp(tmpdir)
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXX"))
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 'ssh_XXXXXX'))
if fd then
vim.loop.fs_close(fd)
end
@ -378,9 +378,9 @@ M.read_file = function(bufnr)
scp({ scp_url, tmpfile }, function(err)
loading.set_loading(bufnr, false)
vim.bo[bufnr].modifiable = true
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufReadPre', bufname }, mods = { silent = true } })
if err then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, "\n"))
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, '\n'))
else
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {})
vim.api.nvim_buf_call(bufnr, function()
@ -394,9 +394,9 @@ M.read_file = function(bufnr)
if filetype then
vim.bo[bufnr].filetype = filetype
end
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufReadPost', bufname }, mods = { silent = true } })
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
vim.keymap.set("n", "gf", M.goto_file, { buffer = bufnr })
vim.keymap.set('n', 'gf', M.goto_file, { buffer = bufnr })
end)
end
@ -405,14 +405,14 @@ M.write_file = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr)
local url = M.parse_url(bufname)
local scp_url = url_to_scp(url)
local cache_dir = vim.fn.stdpath("cache")
assert(type(cache_dir) == "string")
local tmpdir = fs.join(cache_dir, "oil")
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXXXX"))
local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, 'oil')
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 'ssh_XXXXXXXX'))
if fd then
vim.loop.fs_close(fd)
end
vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufWritePre', bufname }, mods = { silent = true } })
vim.bo[bufnr].modifiable = false
vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } })
local tmp_bufnr = vim.fn.bufadd(tmpfile)
@ -420,10 +420,10 @@ M.write_file = function(bufnr)
scp({ tmpfile, scp_url }, function(err)
vim.bo[bufnr].modifiable = true
if err then
vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR)
vim.notify(string.format('Error writing file: %s', err), vim.log.levels.ERROR)
else
vim.bo[bufnr].modified = false
vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufWritePost', bufname }, mods = { silent = true } })
end
vim.loop.fs_unlink(tmpfile)
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
@ -432,7 +432,7 @@ end
M.goto_file = function()
local url = M.parse_url(vim.api.nvim_buf_get_name(0))
local fname = vim.fn.expand("<cfile>")
local fname = vim.fn.expand('<cfile>')
local fullpath = fname
if not fs.is_absolute(fname) then
local pardir = vim.fs.dirname(url.path)
@ -459,7 +459,7 @@ M.goto_file = function()
vim.cmd.edit({ args = { url_to_str(url) } })
return
end
for suffix in vim.gsplit(vim.o.suffixesadd, ",", { plain = true, trimempty = true }) do
for suffix in vim.gsplit(vim.o.suffixesadd, ',', { plain = true, trimempty = true }) do
local suffixname = basename .. suffix
if name_map[suffixname] then
url.path = fullpath .. suffix

View file

@ -1,6 +1,6 @@
local config = require("oil.config")
local layout = require("oil.layout")
local util = require("oil.util")
local config = require('oil.config')
local layout = require('oil.layout')
local util = require('oil.util')
---@class (exact) oil.sshCommand
---@field cmd string|string[]
@ -24,12 +24,12 @@ local function output_extend(agg, output)
local start = #agg
if vim.tbl_isempty(agg) then
for _, line in ipairs(output) do
line = line:gsub("\r", "")
line = line:gsub('\r', '')
table.insert(agg, line)
end
else
for i, v in ipairs(output) do
v = v:gsub("\r", "")
v = v:gsub('\r', '')
if i == 1 then
agg[#agg] = agg[#agg] .. v
else
@ -53,7 +53,7 @@ local function get_last_lines(bufnr, num_lines)
vim.api.nvim_buf_get_lines(bufnr, end_line - need_lines, end_line, false),
lines
)
while not vim.tbl_isempty(lines) and lines[#lines]:match("^%s*$") do
while not vim.tbl_isempty(lines) and lines[#lines]:match('^%s*$') do
table.remove(lines)
end
end_line = end_line - need_lines
@ -66,14 +66,14 @@ end
function SSHConnection.create_ssh_command(url)
local host = url.host
if url.user then
host = url.user .. "@" .. host
host = url.user .. '@' .. host
end
local command = {
"ssh",
'ssh',
host,
}
if url.port then
table.insert(command, "-p")
table.insert(command, '-p')
table.insert(command, url.port)
end
return command
@ -84,8 +84,8 @@ end
function SSHConnection.new(url)
local command = SSHConnection.create_ssh_command(url)
vim.list_extend(command, {
"/bin/sh",
"-c",
'/bin/sh',
'-c',
-- HACK: For some reason in my testing if I just have "echo READY" it doesn't appear, but if I echo
-- anything prior to that, it *will* appear. The first line gets swallowed.
"echo '_make_newline_'; echo '===READY==='; exec /bin/sh",
@ -112,7 +112,7 @@ function SSHConnection.new(url)
})
end)
self.term_id = term_id
vim.api.nvim_chan_send(term_id, string.format("ssh %s\r\n", url.host))
vim.api.nvim_chan_send(term_id, string.format('ssh %s\r\n', url.host))
util.hack_around_termopen_autocmd(mode)
-- If it takes more than 2 seconds to connect, pop open the terminal
@ -125,7 +125,7 @@ function SSHConnection.new(url)
local jid = vim.fn.jobstart(command, {
pty = true, -- This is require for interactivity
on_stdout = function(j, output)
pcall(vim.api.nvim_chan_send, self.term_id, table.concat(output, "\r\n"))
pcall(vim.api.nvim_chan_send, self.term_id, table.concat(output, '\r\n'))
---@diagnostic disable-next-line: invisible
local new_i_start = output_extend(self._stdout, output)
self:_handle_output(new_i_start)
@ -134,12 +134,12 @@ function SSHConnection.new(url)
pcall(
vim.api.nvim_chan_send,
self.term_id,
string.format("\r\n[Process exited %d]\r\n", code)
string.format('\r\n[Process exited %d]\r\n', code)
)
-- Defer to allow the deferred terminal output handling to kick in first
vim.defer_fn(function()
if code == 0 then
self:_set_connection_error("SSH connection terminated gracefully")
self:_set_connection_error('SSH connection terminated gracefully')
else
self:_set_connection_error(
'Unknown SSH error\nTo see more, run :lua require("oil.adapters.ssh").open_terminal()'
@ -156,23 +156,23 @@ function SSHConnection.new(url)
else
self.jid = jid
end
self:run("id -u", function(err, lines)
self:run('id -u', function(err, lines)
if err then
vim.notify(string.format("Error fetching ssh connection user: %s", err), vim.log.levels.WARN)
vim.notify(string.format('Error fetching ssh connection user: %s', err), vim.log.levels.WARN)
else
assert(lines)
self.meta.user = vim.trim(table.concat(lines, ""))
self.meta.user = vim.trim(table.concat(lines, ''))
end
end)
self:run("id -G", function(err, lines)
self:run('id -G', function(err, lines)
if err then
vim.notify(
string.format("Error fetching ssh connection user groups: %s", err),
string.format('Error fetching ssh connection user groups: %s', err),
vim.log.levels.WARN
)
else
assert(lines)
self.meta.groups = vim.split(table.concat(lines, ""), "%s+", { trimempty = true })
self.meta.groups = vim.split(table.concat(lines, ''), '%s+', { trimempty = true })
end
end)
@ -197,7 +197,7 @@ function SSHConnection:_handle_output(start_i)
if not self.connected then
for i = start_i, #self._stdout - 1 do
local line = self._stdout[i]
if line == "===READY===" then
if line == '===READY===' then
if self.term_winid then
if vim.api.nvim_win_is_valid(self.term_winid) then
vim.api.nvim_win_close(self.term_winid, true)
@ -215,7 +215,7 @@ function SSHConnection:_handle_output(start_i)
for i = start_i, #self._stdout - 1 do
---@type string
local line = self._stdout[i]
if line:match("^===BEGIN===%s*$") then
if line:match('^===BEGIN===%s*$') then
self._stdout = util.tbl_slice(self._stdout, i + 1)
self:_handle_output(1)
return
@ -223,15 +223,15 @@ function SSHConnection:_handle_output(start_i)
-- We can't be as strict with the matching (^$) because since we're using a pty the stdout and
-- stderr can be interleaved. If the command had an error, the stderr may interfere with a
-- clean print of the done line.
local exit_code = line:match("===DONE%((%d+)%)===")
local exit_code = line:match('===DONE%((%d+)%)===')
if exit_code then
local output = util.tbl_slice(self._stdout, 1, i - 1)
local cb = self.commands[1].cb
self._stdout = util.tbl_slice(self._stdout, i + 1)
if exit_code == "0" then
if exit_code == '0' then
cb(nil, output)
else
cb(exit_code .. ": " .. table.concat(output, "\n"), output)
cb(exit_code .. ': ' .. table.concat(output, '\n'), output)
end
table.remove(self.commands, 1)
self:_handle_output(1)
@ -244,16 +244,17 @@ function SSHConnection:_handle_output(start_i)
local function check_last_line()
local last_lines = get_last_lines(self.term_bufnr, 1)
local last_line = last_lines[1]
if last_line:match("^Are you sure you want to continue connecting") then
if last_line:match('^Are you sure you want to continue connecting') then
self:open_terminal()
elseif last_line:match("Password:%s*$") then
-- selene: allow(if_same_then_else)
elseif last_line:match('Password:%s*$') then
self:open_terminal()
elseif last_line:match(": Permission denied %(.+%)%.") then
self:_set_connection_error(last_line:match(": (Permission denied %(.+%).)"))
elseif last_line:match("^ssh: .*Connection refused%s*$") then
self:_set_connection_error("Connection refused")
elseif last_line:match("^Connection to .+ closed by remote host.%s*$") then
self:_set_connection_error("Connection closed by remote host")
elseif last_line:match(': Permission denied %(.+%)%.') then
self:_set_connection_error(last_line:match(': (Permission denied %(.+%).)'))
elseif last_line:match('^ssh: .*Connection refused%s*$') then
self:_set_connection_error('Connection refused')
elseif last_line:match('^Connection to .+ closed by remote host.%s*$') then
self:_set_connection_error('Connection closed by remote host')
end
end
-- We have to defer this so the terminal buffer has time to update
@ -273,12 +274,12 @@ function SSHConnection:open_terminal()
local row = math.floor((total_height - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
self.term_winid = vim.api.nvim_open_win(self.term_bufnr, true, {
relative = "editor",
relative = 'editor',
width = width,
height = height,
row = row,
col = col,
style = "minimal",
style = 'minimal',
border = config.ssh.border,
})
vim.cmd.startinsert()

View file

@ -1,8 +1,8 @@
local SSHConnection = require("oil.adapters.ssh.connection")
local cache = require("oil.cache")
local constants = require("oil.constants")
local permissions = require("oil.adapters.files.permissions")
local util = require("oil.util")
local SSHConnection = require('oil.adapters.ssh.connection')
local cache = require('oil.cache')
local constants = require('oil.constants')
local permissions = require('oil.adapters.files.permissions')
local util = require('oil.util')
---@class (exact) oil.sshFs
---@field new fun(url: oil.sshUrl): oil.sshFs
@ -13,13 +13,13 @@ local FIELD_TYPE = constants.FIELD_TYPE
local FIELD_META = constants.FIELD_META
local typechar_map = {
l = "link",
d = "directory",
p = "fifo",
s = "socket",
["-"] = "file",
c = "file", -- character special file
b = "file", -- block special file
l = 'link',
d = 'directory',
p = 'fifo',
s = 'socket',
['-'] = 'file',
c = 'file', -- character special file
b = 'file', -- block special file
}
---@param line string
---@return string Name of entry
@ -27,11 +27,11 @@ local typechar_map = {
---@return table Metadata for entry
local function parse_ls_line(line)
local typechar, perms, refcount, user, group, rem =
line:match("^(.)(%S+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(.*)$")
line:match('^(.)(%S+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(.*)$')
if not typechar then
error(string.format("Could not parse '%s'", line))
end
local type = typechar_map[typechar] or "file"
local type = typechar_map[typechar] or 'file'
local meta = {
user = user,
@ -40,26 +40,26 @@ local function parse_ls_line(line)
refcount = tonumber(refcount),
}
local name, size, date, major, minor
if typechar == "c" or typechar == "b" then
major, minor, date, name = rem:match("^(%d+)%s*,%s*(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)")
if typechar == 'c' or typechar == 'b' then
major, minor, date, name = rem:match('^(%d+)%s*,%s*(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)')
if name == nil then
major, minor, date, name =
rem:match("^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
rem:match('^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)')
end
meta.major = tonumber(major)
meta.minor = tonumber(minor)
else
size, date, name = rem:match("^(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)")
size, date, name = rem:match('^(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)')
if name == nil then
size, date, name = rem:match("^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
size, date, name = rem:match('^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)')
end
meta.size = tonumber(size)
end
meta.iso_modified_date = date
if type == "link" then
if type == 'link' then
local link
name, link = unpack(vim.split(name, " -> ", { plain = true }))
if vim.endswith(link, "/") then
name, link = unpack(vim.split(name, ' -> ', { plain = true }))
if vim.endswith(link, '/') then
link = link:sub(1, #link - 1)
end
meta.link = link
@ -94,7 +94,7 @@ end
---@param callback fun(err: nil|string)
function SSHFS:chmod(value, path, callback)
local octal = permissions.mode_to_octal_str(value)
self.conn:run(string.format("chmod %s %s", octal, shellescape(path)), callback)
self.conn:run(string.format('chmod %s %s', octal, shellescape(path)), callback)
end
function SSHFS:open_terminal()
@ -114,24 +114,24 @@ function SSHFS:realpath(path, callback)
return callback(err)
end
assert(lines)
local abspath = table.concat(lines, "")
local abspath = table.concat(lines, '')
-- If the path was "." then the abspath might be /path/to/., so we need to trim that final '.'
if vim.endswith(abspath, ".") then
if vim.endswith(abspath, '.') then
abspath = abspath:sub(1, #abspath - 1)
end
self.conn:run(
string.format("LC_ALL=C ls -land --color=never %s", shellescape(abspath)),
string.format('LC_ALL=C ls -land --color=never %s', shellescape(abspath)),
function(ls_err, ls_lines)
local type
if ls_err then
-- If the file doesn't exist, treat it like a not-yet-existing directory
type = "directory"
type = 'directory'
else
assert(ls_lines)
local _
_, type = parse_ls_line(ls_lines[1])
end
if type == "directory" then
if type == 'directory' then
abspath = util.addslash(abspath)
end
callback(nil, abspath)
@ -146,13 +146,13 @@ local dir_meta = {}
---@param path string
---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
function SSHFS:list_dir(url, path, callback)
local path_postfix = ""
if path ~= "" then
path_postfix = string.format(" %s", shellescape(path))
local path_postfix = ''
if path ~= '' then
path_postfix = string.format(' %s', shellescape(path))
end
self.conn:run("LC_ALL=C ls -lan --color=never" .. path_postfix, function(err, lines)
self.conn:run('LC_ALL=C ls -lan --color=never' .. path_postfix, function(err, lines)
if err then
if err:match("No such file or directory%s*$") then
if err:match('No such file or directory%s*$') 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 callback()
@ -165,12 +165,12 @@ function SSHFS:list_dir(url, path, callback)
local entries = {}
local cache_entries = {}
for _, line in ipairs(lines) do
if line ~= "" and not line:match("^total") then
if line ~= '' and not line:match('^total') then
local name, type, meta = parse_ls_line(line)
if name == "." then
if name == '.' then
dir_meta[url] = meta
elseif name ~= ".." then
if type == "link" then
elseif name ~= '..' then
if type == 'link' then
any_links = true
end
local cache_entry = cache.create_entry(url, name, type)
@ -184,19 +184,19 @@ function SSHFS:list_dir(url, path, callback)
-- If there were any soft links, then we need to run another ls command with -L so that we can
-- resolve the type of the link target
self.conn:run(
"LC_ALL=C ls -naLl --color=never" .. path_postfix .. " 2> /dev/null",
'LC_ALL=C ls -naLl --color=never' .. path_postfix .. ' 2> /dev/null',
function(link_err, link_lines)
-- Ignore exit code 1. That just means one of the links could not be resolved.
if link_err and not link_err:match("^1:") then
if link_err and not link_err:match('^1:') then
return callback(link_err)
end
assert(link_lines)
for _, line in ipairs(link_lines) do
if line ~= "" and not line:match("^total") then
if line ~= '' and not line:match('^total') then
local ok, name, type, meta = pcall(parse_ls_line, line)
if ok and name ~= "." and name ~= ".." then
if ok and name ~= '.' and name ~= '..' then
local cache_entry = entries[name]
if cache_entry[FIELD_TYPE] == "link" then
if cache_entry[FIELD_TYPE] == 'link' then
cache_entry[FIELD_META].link_stat = {
type = type,
size = meta.size,
@ -217,40 +217,40 @@ end
---@param path string
---@param callback fun(err: nil|string)
function SSHFS:mkdir(path, callback)
self.conn:run(string.format("mkdir -p %s", shellescape(path)), callback)
self.conn:run(string.format('mkdir -p %s', shellescape(path)), callback)
end
---@param path string
---@param callback fun(err: nil|string)
function SSHFS:touch(path, callback)
self.conn:run(string.format("touch %s", shellescape(path)), callback)
self.conn:run(string.format('touch %s', shellescape(path)), callback)
end
---@param path string
---@param link string
---@param callback fun(err: nil|string)
function SSHFS:mklink(path, link, callback)
self.conn:run(string.format("ln -s %s %s", shellescape(link), shellescape(path)), callback)
self.conn:run(string.format('ln -s %s %s', shellescape(link), shellescape(path)), callback)
end
---@param path string
---@param callback fun(err: nil|string)
function SSHFS:rm(path, callback)
self.conn:run(string.format("rm -rf %s", shellescape(path)), callback)
self.conn:run(string.format('rm -rf %s', shellescape(path)), callback)
end
---@param src string
---@param dest string
---@param callback fun(err: nil|string)
function SSHFS:mv(src, dest, callback)
self.conn:run(string.format("mv %s %s", shellescape(src), shellescape(dest)), callback)
self.conn:run(string.format('mv %s %s', shellescape(src), shellescape(dest)), callback)
end
---@param src string
---@param dest string
---@param callback fun(err: nil|string)
function SSHFS:cp(src, dest, callback)
self.conn:run(string.format("cp -r %s %s", shellescape(src), shellescape(dest)), callback)
self.conn:run(string.format('cp -r %s %s', shellescape(src), shellescape(dest)), callback)
end
function SSHFS:get_dir_meta(url)

View file

@ -1,5 +1,5 @@
local cache = require("oil.cache")
local util = require("oil.util")
local cache = require('oil.cache')
local util = require('oil.util')
local M = {}
---@param url string
@ -32,24 +32,24 @@ end
---@param entry_type oil.EntryType
---@return oil.InternalEntry
M.test_set = function(path, entry_type)
if path == "/" then
if path == '/' then
return {}
end
local parent = vim.fn.fnamemodify(path, ":h")
local parent = vim.fn.fnamemodify(path, ':h')
if parent ~= path then
M.test_set(parent, "directory")
M.test_set(parent, 'directory')
end
parent = util.addslash(parent)
if not dir_listing[parent] then
dir_listing[parent] = {}
end
local name = vim.fn.fnamemodify(path, ":t")
local name = vim.fn.fnamemodify(path, ':t')
local entry = {
name = name,
entry_type = entry_type,
}
table.insert(dir_listing[parent], entry)
local parent_url = "oil-test://" .. parent
local parent_url = 'oil-test://' .. parent
return cache.create_and_store_entry(parent_url, entry.name, entry.entry_type)
end
@ -68,12 +68,12 @@ end
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == "create" or action.type == "delete" then
return string.format("%s %s", action.type:upper(), action.url)
elseif action.type == "move" or action.type == "copy" then
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url)
if action.type == 'create' or action.type == 'delete' then
return string.format('%s %s', action.type:upper(), action.url)
elseif action.type == 'move' or action.type == 'copy' then
return string.format(' %s %s -> %s', action.type:upper(), action.src_url, action.dest_url)
else
error("Bad action type")
error('Bad action type')
end
end

View file

@ -1,9 +1,9 @@
local fs = require("oil.fs")
local fs = require('oil.fs')
if fs.is_mac then
return require("oil.adapters.trash.mac")
return require('oil.adapters.trash.mac')
elseif fs.is_windows then
return require("oil.adapters.trash.windows")
return require('oil.adapters.trash.windows')
else
return require("oil.adapters.trash.freedesktop")
return require('oil.adapters.trash.freedesktop')
end

View file

@ -1,11 +1,11 @@
-- Based on the FreeDesktop.org trash specification
-- https://specifications.freedesktop.org/trash/1.0/
local cache = require("oil.cache")
local config = require("oil.config")
local constants = require("oil.constants")
local files = require("oil.adapters.files")
local fs = require("oil.fs")
local util = require("oil.util")
local cache = require('oil.cache')
local config = require('oil.config')
local constants = require('oil.constants')
local files = require('oil.adapters.files')
local fs = require('oil.fs')
local util = require('oil.util')
local uv = vim.uv or vim.loop
local FIELD_META = constants.FIELD_META
@ -14,8 +14,8 @@ local M = {}
local function ensure_trash_dir(path)
local mode = 448 -- 0700
fs.mkdirp(fs.join(path, "info"), mode)
fs.mkdirp(fs.join(path, "files"), mode)
fs.mkdirp(fs.join(path, 'info'), mode)
fs.mkdirp(fs.join(path, 'files'), mode)
end
---Gets the location of the home trash dir, creating it if necessary
@ -23,9 +23,9 @@ end
local function get_home_trash_dir()
local xdg_home = vim.env.XDG_DATA_HOME
if not xdg_home then
xdg_home = fs.join(assert(uv.os_homedir()), ".local", "share")
xdg_home = fs.join(assert(uv.os_homedir()), '.local', 'share')
end
local trash_dir = fs.join(xdg_home, "Trash")
local trash_dir = fs.join(xdg_home, 'Trash')
ensure_trash_dir(trash_dir)
return trash_dir
end
@ -43,13 +43,13 @@ end
local function get_top_trash_dirs(path)
local dirs = {}
local dev = (uv.fs_lstat(path) or {}).dev
local top_trash_dirs = vim.fs.find(".Trash", { upward = true, path = path, limit = math.huge })
local top_trash_dirs = vim.fs.find('.Trash', { upward = true, path = path, limit = math.huge })
for _, top_trash_dir in ipairs(top_trash_dirs) do
local stat = uv.fs_lstat(top_trash_dir)
if stat and not dev then
dev = stat.dev
end
if stat and stat.dev == dev and stat.type == "directory" and is_sticky(stat.mode) then
if stat and stat.dev == dev and stat.type == 'directory' and is_sticky(stat.mode) then
local trash_dir = fs.join(top_trash_dir, tostring(uv.getuid()))
ensure_trash_dir(trash_dir)
table.insert(dirs, trash_dir)
@ -58,7 +58,7 @@ local function get_top_trash_dirs(path)
-- Also search for the .Trash-$uid
top_trash_dirs = vim.fs.find(
string.format(".Trash-%d", uv.getuid()),
string.format('.Trash-%d', uv.getuid()),
{ upward = true, path = path, limit = math.huge }
)
for _, top_trash_dir in ipairs(top_trash_dirs) do
@ -91,14 +91,14 @@ local function get_write_trash_dir(path)
return top_trash_dirs[1]
end
local parent = vim.fn.fnamemodify(path, ":h")
local next_parent = vim.fn.fnamemodify(parent, ":h")
local parent = vim.fn.fnamemodify(path, ':h')
local next_parent = vim.fn.fnamemodify(parent, ':h')
while parent ~= next_parent and uv.fs_lstat(next_parent).dev == dev do
parent = next_parent
next_parent = vim.fn.fnamemodify(parent, ":h")
next_parent = vim.fn.fnamemodify(parent, ':h')
end
local top_trash = fs.join(parent, string.format(".Trash-%d", uv.getuid()))
local top_trash = fs.join(parent, string.format('.Trash-%d', uv.getuid()))
ensure_trash_dir(top_trash)
return top_trash
end
@ -116,7 +116,7 @@ end
M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url)
assert(path)
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p")
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p')
uv.fs_realpath(
os_path,
vim.schedule_wrap(function(err, new_os_path)
@ -140,10 +140,10 @@ M.get_entry_path = function(url, entry, cb)
return
end
local path = fs.os_to_posix_path(trash_info.trash_file)
if meta.stat.type == "directory" then
if meta.stat.type == 'directory' then
path = util.addslash(path)
end
cb("oil://" .. path)
cb('oil://' .. path)
end
---@class oil.TrashInfo
@ -156,10 +156,10 @@ end
---@param info_file string
---@param cb fun(err?: string, info?: oil.TrashInfo)
local function read_trash_info(info_file, cb)
if not vim.endswith(info_file, ".trashinfo") then
return cb("File is not .trashinfo")
if not vim.endswith(info_file, '.trashinfo') then
return cb('File is not .trashinfo')
end
uv.fs_open(info_file, "r", 448, function(err, fd)
uv.fs_open(info_file, 'r', 448, function(err, fd)
if err then
return cb(err)
end
@ -182,32 +182,32 @@ local function read_trash_info(info_file, cb)
local trash_info = {
info_file = info_file,
}
local lines = vim.split(content, "\r?\n")
if lines[1] ~= "[Trash Info]" then
return cb("File missing [Trash Info] header")
local lines = vim.split(content, '\r?\n')
if lines[1] ~= '[Trash Info]' then
return cb('File missing [Trash Info] header')
end
local trash_base = vim.fn.fnamemodify(info_file, ":h:h")
local trash_base = vim.fn.fnamemodify(info_file, ':h:h')
for _, line in ipairs(lines) do
local key, value = unpack(vim.split(line, "=", { plain = true, trimempty = true }))
if key == "Path" and not trash_info.original_path then
if not vim.startswith(value, "/") then
local key, value = unpack(vim.split(line, '=', { plain = true, trimempty = true }))
if key == 'Path' and not trash_info.original_path then
if not vim.startswith(value, '/') then
value = fs.join(trash_base, value)
end
trash_info.original_path = value
elseif key == "DeletionDate" and not trash_info.deletion_date then
trash_info.deletion_date = vim.fn.strptime("%Y-%m-%dT%H:%M:%S", value)
elseif key == 'DeletionDate' and not trash_info.deletion_date then
trash_info.deletion_date = vim.fn.strptime('%Y-%m-%dT%H:%M:%S', value)
end
end
if not trash_info.original_path or not trash_info.deletion_date then
return cb("File missing required fields")
return cb('File missing required fields')
end
local basename = vim.fn.fnamemodify(info_file, ":t:r")
trash_info.trash_file = fs.join(trash_base, "files", basename)
local basename = vim.fn.fnamemodify(info_file, ':t:r')
trash_info.trash_file = fs.join(trash_base, 'files', basename)
uv.fs_lstat(trash_info.trash_file, function(trash_stat_err, trash_stat)
if trash_stat_err then
cb(".trashinfo file points to non-existant file")
cb('.trashinfo file points to non-existant file')
else
trash_info.stat = trash_stat
---@cast trash_info oil.TrashInfo
@ -244,14 +244,14 @@ M.list = function(url, column_defs, cb)
-- The first trash dir is a special case; it is in the home directory and we should only show
-- all entries if we are in the top root path "/"
if trash_idx == 1 then
show_all_files = path == "/"
show_all_files = path == '/'
end
local info_dir = fs.join(trash_dir, "info")
local info_dir = fs.join(trash_dir, 'info')
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(info_dir, function(open_err, fd)
if open_err then
if open_err:match("^ENOENT: no such file or directory") 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 read_next_trash_dir()
@ -286,12 +286,12 @@ M.list = function(url, column_defs, cb)
-- files.
poll()
else
local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ":h"))
local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ':h'))
if path == parent or show_all_files then
local name = vim.fn.fnamemodify(info.trash_file, ":t")
local name = vim.fn.fnamemodify(info.trash_file, ':t')
---@diagnostic disable-next-line: undefined-field
local cache_entry = cache.create_entry(url, name, info.stat.type)
local display_name = vim.fn.fnamemodify(info.original_path, ":t")
local display_name = vim.fn.fnamemodify(info.original_path, ':t')
cache_entry[FIELD_META] = {
stat = info.stat,
trash_info = info,
@ -302,12 +302,12 @@ M.list = function(url, column_defs, cb)
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name)
while next_par ~= "." do
while next_par ~= '.' do
name = next_par
next_par = vim.fs.dirname(name)
end
---@diagnostic disable-next-line: undefined-field
local cache_entry = cache.create_entry(url, name, "directory")
local cache_entry = cache.create_entry(url, name, 'directory')
cache_entry[FIELD_META] = {
stat = info.stat,
@ -348,7 +348,7 @@ local file_columns = {}
local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function()
current_year = vim.fn.strftime("%Y")
current_year = vim.fn.strftime('%Y')
end)
file_columns.mtime = {
@ -368,11 +368,11 @@ file_columns.mtime = {
if fmt then
ret = vim.fn.strftime(fmt, time)
else
local year = vim.fn.strftime("%Y", time)
local year = vim.fn.strftime('%Y', time)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", time)
ret = vim.fn.strftime('%b %d %Y', time)
else
ret = vim.fn.strftime("%b %d %H:%M", time)
ret = vim.fn.strftime('%b %d %H:%M', time)
end
end
return ret
@ -393,11 +393,11 @@ file_columns.mtime = {
local fmt = conf and conf.format
local pattern
if fmt then
pattern = fmt:gsub("%%.", "%%S+")
pattern = fmt:gsub('%%.', '%%S+')
else
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
pattern = '%S+%s+%d+%s+%d%d:?%d%d'
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
return line:match('^(' .. pattern .. ')%s+(.+)$')
end,
}
@ -407,25 +407,26 @@ M.get_column = function(name)
return file_columns[name]
end
M.supported_cross_adapter_actions = { files = "move" }
M.supported_cross_adapter_actions = { files = 'move' }
---@param action oil.Action
---@return boolean
M.filter_action = function(action)
if action.type == "create" then
if action.type == 'create' then
return false
elseif action.type == "delete" then
elseif action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
return meta ~= nil and meta.trash_info ~= nil
elseif action.type == "move" then
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))
return src_adapter.name == "files" or dest_adapter.name == "files"
elseif action.type == "copy" then
return src_adapter.name == 'files' or dest_adapter.name == 'files'
-- selene: allow(if_same_then_else)
elseif 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))
return src_adapter.name == "files" or dest_adapter.name == "files"
return src_adapter.name == 'files' or dest_adapter.name == 'files'
else
error(string.format("Bad action type '%s'", action.type))
end
@ -434,7 +435,7 @@ end
---@param err oil.ParseError
---@return boolean
M.filter_error = function(err)
if err.message == "Duplicate filename" then
if err.message == 'Duplicate filename' then
return false
end
return true
@ -443,44 +444,44 @@ end
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == "delete" then
if action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.TrashInfo
local trash_info = assert(meta).trash_info
local short_path = fs.shorten_path(trash_info.original_path)
return string.format(" PURGE %s", short_path)
elseif action.type == "move" then
return string.format(' PURGE %s', short_path)
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
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
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)
return string.format('RESTORE %s', short_path)
else
error("Must be moving files into or out of trash")
error('Must be moving files into or out of trash')
end
elseif action.type == "copy" then
elseif 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))
if src_adapter.name == "files" then
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(" COPY %s -> TRASH", short_path)
elseif dest_adapter.name == "files" then
return string.format(' COPY %s -> TRASH', 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)
return string.format('RESTORE %s', short_path)
else
error("Must be copying files into or out of trash")
error('Must be copying files into or out of trash')
end
else
error(string.format("Bad action type '%s'", action.type))
@ -490,7 +491,7 @@ end
---@param trash_info oil.TrashInfo
---@param cb fun(err?: string)
local function purge(trash_info, cb)
fs.recursive_delete("file", trash_info.info_file, function(err)
fs.recursive_delete('file', trash_info.info_file, function(err)
if err then
return cb(err)
end
@ -505,15 +506,15 @@ end
local function write_info_file(path, info_path, cb)
uv.fs_open(
info_path,
"w",
'w',
448,
vim.schedule_wrap(function(err, fd)
if err then
return cb(err)
end
assert(fd)
local deletion_date = vim.fn.strftime("%Y-%m-%dT%H:%M:%S")
local contents = string.format("[Trash Info]\nPath=%s\nDeletionDate=%s", path, deletion_date)
local deletion_date = vim.fn.strftime('%Y-%m-%dT%H:%M:%S')
local contents = string.format('[Trash Info]\nPath=%s\nDeletionDate=%s', path, deletion_date)
uv.fs_write(fd, contents, function(write_err)
uv.fs_close(fd, function(close_err)
cb(write_err or close_err)
@ -529,9 +530,9 @@ local function create_trash_info(path, cb)
local trash_dir = get_write_trash_dir(path)
local basename = vim.fs.basename(path)
local now = os.time()
local name = string.format("%s-%d.%d", basename, now, math.random(100000, 999999))
local dest_path = fs.join(trash_dir, "files", name)
local dest_info = fs.join(trash_dir, "info", name .. ".trashinfo")
local name = string.format('%s-%d.%d', basename, now, math.random(100000, 999999))
local dest_path = fs.join(trash_dir, 'files', name)
local dest_info = fs.join(trash_dir, 'info', name .. '.trashinfo')
uv.fs_lstat(path, function(err, stat)
if err then
return cb(err)
@ -557,19 +558,19 @@ end
---@param action oil.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
if action.type == "delete" then
if action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.TrashInfo
local trash_info = assert(meta).trash_info
purge(trash_info, cb)
elseif action.type == "move" then
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
if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url)
M.delete_to_trash(assert(path), cb)
elseif dest_adapter.name == "files" then
elseif dest_adapter.name == 'files' then
-- Restore
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
@ -584,23 +585,23 @@ M.perform_action = function(action, cb)
uv.fs_unlink(trash_info.info_file, cb)
end)
else
error("Must be moving files into or out of trash")
error('Must be moving files into or out of trash')
end
elseif action.type == "copy" then
elseif 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))
if src_adapter.name == "files" then
if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url)
assert(path)
create_trash_info(path, function(err, trash_info)
if err then
cb(err)
else
local stat_type = trash_info.stat.type or "unknown"
local stat_type = trash_info.stat.type or 'unknown'
fs.recursive_copy(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
end
end)
elseif dest_adapter.name == "files" then
elseif dest_adapter.name == 'files' then
-- Restore
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
@ -610,10 +611,10 @@ M.perform_action = function(action, cb)
local trash_info = assert(meta).trash_info
fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
else
error("Must be moving files into or out of trash")
error('Must be moving files into or out of trash')
end
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end
@ -624,7 +625,7 @@ M.delete_to_trash = function(path, cb)
if err then
cb(err)
else
local stat_type = trash_info.stat.type or "unknown"
local stat_type = trash_info.stat.type or 'unknown'
fs.recursive_move(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
end
end)

View file

@ -1,8 +1,8 @@
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 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
@ -15,7 +15,7 @@ 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")
local trash_dir = fs.join(assert(uv.os_homedir()), '.Trash')
touch_dir(trash_dir)
return trash_dir
end
@ -25,7 +25,7 @@ end
M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url)
assert(path)
callback(scheme .. "/")
callback(scheme .. '/')
end
---@param url string
@ -34,8 +34,8 @@ end
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
if entry.type == 'directory' then
path = 'oil://' .. path
end
cb(path)
end
@ -51,7 +51,7 @@ M.list = function(url, column_defs, cb)
---@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 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()
@ -111,35 +111,35 @@ M.get_column = function(name)
return nil
end
M.supported_cross_adapter_actions = { files = "move" }
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
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
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
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)
return string.format('RESTORE %s', short_path)
else
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url)
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)
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")
error('Bad action type')
end
end
@ -147,20 +147,20 @@ end
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
local trash_dir = get_trash_dir()
if action.type == "create" then
if action.type == 'create' then
local _, path = util.parse_url(action.url)
assert(path)
path = trash_dir .. path
if action.entry_type == "directory" then
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
if not err or err:match('^EEXIST:') then
cb()
else
cb(err)
end
end) -- 0755
elseif action.entry_type == "link" and action.link then
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
@ -168,33 +168,33 @@ M.perform_action = function(action, cb)
else
fs.touch(path, config.new_file_mode, cb)
end
elseif action.type == "delete" then
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
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
if src_adapter.name == 'files' then
dest_path = trash_dir .. dest_path
elseif dest_adapter.name == "files" then
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
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))
cb(string.format('Bad action type: %s', action.type))
end
end
@ -212,8 +212,8 @@ M.delete_to_trash = function(path, cb)
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 })
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)

View file

@ -1,11 +1,11 @@
local util = require("oil.util")
local util = require('oil.util')
local uv = vim.uv or vim.loop
local cache = require("oil.cache")
local config = require("oil.config")
local constants = require("oil.constants")
local files = require("oil.adapters.files")
local fs = require("oil.fs")
local powershell_trash = require("oil.adapters.trash.windows.powershell-trash")
local cache = require('oil.cache')
local config = require('oil.config')
local constants = require('oil.constants')
local files = require('oil.adapters.files')
local fs = require('oil.fs')
local powershell_trash = require('oil.adapters.trash.windows.powershell-trash')
local FIELD_META = constants.FIELD_META
local FIELD_TYPE = constants.FIELD_TYPE
@ -15,22 +15,22 @@ local M = {}
---@return string
local function get_trash_dir()
local cwd = assert(vim.fn.getcwd())
local trash_dir = cwd:sub(1, 3) .. "$Recycle.Bin"
local trash_dir = cwd:sub(1, 3) .. '$Recycle.Bin'
if vim.fn.isdirectory(trash_dir) == 1 then
return trash_dir
end
trash_dir = "C:\\$Recycle.Bin"
trash_dir = 'C:\\$Recycle.Bin'
if vim.fn.isdirectory(trash_dir) == 1 then
return trash_dir
end
error("No trash found")
error('No trash found')
end
---@param path string
---@return string
local win_addslash = function(path)
if not vim.endswith(path, "\\") then
return path .. "\\"
if not vim.endswith(path, '\\') then
return path .. '\\'
else
return path
end
@ -61,7 +61,7 @@ M.list = function(url, column_defs, cb)
local raw_displayed_entries = vim.tbl_filter(
---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string}
function(entry)
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h")))
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ':h')))
local is_in_path = path == parent
local is_subpath = fs.is_subpath(path, parent)
return is_in_path or is_subpath or show_all_files
@ -72,27 +72,27 @@ M.list = function(url, column_defs, cb)
---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string}
---@return {[1]:nil, [2]:string, [3]:string, [4]:{stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}}
function(entry)
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h")))
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ':h')))
--- @type oil.InternalEntry
local cache_entry
if path == parent or show_all_files then
local deleted_file_tail = assert(vim.fn.fnamemodify(entry.Path, ":t"))
local deleted_file_head = assert(vim.fn.fnamemodify(entry.Path, ":h"))
local deleted_file_tail = assert(vim.fn.fnamemodify(entry.Path, ':t'))
local deleted_file_head = assert(vim.fn.fnamemodify(entry.Path, ':h'))
local info_file_head = deleted_file_head
--- @type string?
local info_file
cache_entry =
cache.create_entry(url, deleted_file_tail, entry.IsFolder and "directory" or "file")
cache.create_entry(url, deleted_file_tail, entry.IsFolder and 'directory' or 'file')
-- info_file on windows has the following format: $I<6 char hash>.<extension>
-- the hash is the same for the deleted file and the info file
-- so, we take the hash (and extension) from the deleted file
--
-- see https://superuser.com/questions/368890/how-does-the-recycle-bin-in-windows-work/1736690#1736690
local info_file_tail = deleted_file_tail:match("^%$R(.*)$") --[[@as string?]]
local info_file_tail = deleted_file_tail:match('^%$R(.*)$') --[[@as string?]]
if info_file_tail then
info_file_tail = "$I" .. info_file_tail
info_file = info_file_head .. "\\" .. info_file_tail
info_file_tail = '$I' .. info_file_tail
info_file = info_file_head .. '\\' .. info_file_tail
end
cache_entry[FIELD_META] = {
stat = nil,
@ -109,10 +109,10 @@ M.list = function(url, column_defs, cb)
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name)
while next_par ~= "." do
while next_par ~= '.' do
name = next_par
next_par = vim.fs.dirname(name)
cache_entry = cache.create_entry(url, name, "directory")
cache_entry = cache.create_entry(url, name, 'directory')
cache_entry[FIELD_META] = {}
end
@ -132,7 +132,7 @@ end
local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function()
current_year = vim.fn.strftime("%Y")
current_year = vim.fn.strftime('%Y')
end)
local file_columns = {}
@ -153,11 +153,11 @@ file_columns.mtime = {
if fmt then
ret = vim.fn.strftime(fmt, time)
else
local year = vim.fn.strftime("%Y", time)
local year = vim.fn.strftime('%Y', time)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", time)
ret = vim.fn.strftime('%b %d %Y', time)
else
ret = vim.fn.strftime("%b %d %H:%M", time)
ret = vim.fn.strftime('%b %d %H:%M', time)
end
end
return ret
@ -178,11 +178,11 @@ file_columns.mtime = {
local fmt = conf and conf.format
local pattern
if fmt then
pattern = fmt:gsub("%%.", "%%S+")
pattern = fmt:gsub('%%.', '%%S+')
else
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
pattern = '%S+%s+%d+%s+%d%d:?%d%d'
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
return line:match('^(' .. pattern .. ')%s+(.+)$')
end,
}
@ -195,20 +195,21 @@ end
---@param action oil.Action
---@return boolean
M.filter_action = function(action)
if action.type == "create" then
if action.type == 'create' then
return false
elseif action.type == "delete" then
elseif action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
return meta ~= nil and meta.trash_info ~= nil
elseif action.type == "move" then
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))
return src_adapter.name == "files" or dest_adapter.name == "files"
elseif action.type == "copy" then
return src_adapter.name == 'files' or dest_adapter.name == 'files'
-- selene: allow(if_same_then_else)
elseif 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))
return src_adapter.name == "files" or dest_adapter.name == "files"
return src_adapter.name == 'files' or dest_adapter.name == 'files'
else
error(string.format("Bad action type '%s'", action.type))
end
@ -219,7 +220,7 @@ end
M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url)
assert(path)
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p")
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p')
assert(os_path)
uv.fs_realpath(
os_path,
@ -244,16 +245,16 @@ M.get_entry_path = function(url, entry, cb)
end
local path = fs.os_to_posix_path(trash_info.trash_file)
if entry.type == "directory" then
if entry.type == 'directory' then
path = win_addslash(path)
end
cb("oil://" .. path)
cb('oil://' .. path)
end
---@param err oil.ParseError
---@return boolean
M.filter_error = function(err)
if err.message == "Duplicate filename" then
if err.message == 'Duplicate filename' then
return false
end
return true
@ -262,44 +263,44 @@ end
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == "delete" then
if action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.WindowsTrashInfo
local trash_info = assert(meta).trash_info
local short_path = fs.shorten_path(trash_info.original_path)
return string.format(" PURGE %s", short_path)
elseif action.type == "move" then
return string.format(' PURGE %s', short_path)
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
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
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)
return string.format('RESTORE %s', short_path)
else
error("Must be moving files into or out of trash")
error('Must be moving files into or out of trash')
end
elseif action.type == "copy" then
elseif 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))
if src_adapter.name == "files" then
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(" COPY %s -> TRASH", short_path)
elseif dest_adapter.name == "files" then
return string.format(' COPY %s -> TRASH', 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)
return string.format('RESTORE %s', short_path)
else
error("Must be copying files into or out of trash")
error('Must be copying files into or out of trash')
end
else
error(string.format("Bad action type '%s'", action.type))
@ -309,11 +310,11 @@ end
---@param trash_info oil.WindowsTrashInfo
---@param cb fun(err?: string, raw_entries: oil.WindowsRawEntry[]?)
local purge = function(trash_info, cb)
fs.recursive_delete("file", trash_info.info_file, function(err)
fs.recursive_delete('file', trash_info.info_file, function(err)
if err then
return cb(err)
end
fs.recursive_delete("file", trash_info.trash_file, cb)
fs.recursive_delete('file', trash_info.trash_file, cb)
end)
end
@ -321,7 +322,7 @@ end
---@param type string
---@param cb fun(err?: string, trash_info?: oil.TrashInfo)
local function create_trash_info_and_copy(path, type, cb)
local temp_path = path .. "temp"
local temp_path = path .. 'temp'
-- create a temporary copy on the same location
fs.recursive_copy(
type,
@ -346,19 +347,19 @@ end
---@param action oil.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
if action.type == "delete" then
if action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
local trash_info = meta and meta.trash_info
purge(trash_info, cb)
elseif action.type == "move" then
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
if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url)
M.delete_to_trash(assert(path), cb)
elseif dest_adapter.name == "files" then
elseif dest_adapter.name == 'files' then
-- Restore
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
@ -373,16 +374,16 @@ M.perform_action = function(action, cb)
uv.fs_unlink(trash_info.info_file, cb)
end)
end
elseif action.type == "copy" then
elseif 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))
if src_adapter.name == "files" then
if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url)
assert(path)
path = fs.posix_to_os_path(path)
local entry = assert(cache.get_entry_by_url(action.src_url))
create_trash_info_and_copy(path, entry[FIELD_TYPE], cb)
elseif dest_adapter.name == "files" then
elseif dest_adapter.name == 'files' then
-- Restore
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
@ -392,14 +393,14 @@ M.perform_action = function(action, cb)
local trash_info = meta and meta.trash_info
fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
else
error("Must be moving files into or out of trash")
error('Must be moving files into or out of trash')
end
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end
M.supported_cross_adapter_actions = { files = "move" }
M.supported_cross_adapter_actions = { files = 'move' }
---@param path string
---@param cb fun(err?: string)

View file

@ -41,23 +41,23 @@ function PowershellConnection:_init(init_command)
-- 65001 is the UTF-8 codepage
-- powershell needs to be launched with the UTF-8 codepage to use it for both stdin and stdout
local jid = vim.fn.jobstart({
"cmd",
"/c",
'cmd',
'/c',
'"chcp 65001 && powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -NoExit -Command -"',
}, {
---@param data string[]
on_stdout = function(_, data)
for _, fragment in ipairs(data) do
if fragment:find("===DONE%((%a+)%)===") then
if fragment:find('===DONE%((%a+)%)===') then
self.is_reading_data = false
local output = table.concat(self.stdout, "")
local output = table.concat(self.stdout, '')
local cb = self.commands[1].cb
table.remove(self.commands, 1)
local success = fragment:match("===DONE%((%a+)%)===")
if success == "True" then
local success = fragment:match('===DONE%((%a+)%)===')
if success == 'True' then
cb(nil, output)
elseif success == "False" then
cb(success .. ": " .. output, output)
elseif success == 'False' then
cb(success .. ': ' .. output, output)
end
self.stdout = {}
self:_consume()

View file

@ -1,5 +1,5 @@
-- A wrapper around trash operations using windows powershell
local Powershell = require("oil.adapters.trash.windows.powershell-connection")
local Powershell = require('oil.adapters.trash.windows.powershell-connection')
---@class oil.WindowsRawEntry
---@field IsFolder boolean