feat: first draft
This commit is contained in:
parent
bf2dfb970d
commit
fefd6ad5e4
48 changed files with 7201 additions and 1 deletions
406
lua/oil/adapters/files.lua
Normal file
406
lua/oil/adapters/files.lua
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
local cache = require("oil.cache")
|
||||
local columns = require("oil.columns")
|
||||
local config = require("oil.config")
|
||||
local fs = require("oil.fs")
|
||||
local permissions = require("oil.adapters.files.permissions")
|
||||
local util = require("oil.util")
|
||||
local FIELD = require("oil.constants").FIELD
|
||||
local M = {}
|
||||
|
||||
local function read_link_data(path, cb)
|
||||
vim.loop.fs_readlink(
|
||||
path,
|
||||
vim.schedule_wrap(function(link_err, link)
|
||||
if link_err then
|
||||
cb(link_err)
|
||||
else
|
||||
local stat_path = link
|
||||
if not fs.is_absolute(link) then
|
||||
stat_path = fs.join(vim.fn.fnamemodify(path, ":h"), link)
|
||||
end
|
||||
vim.loop.fs_stat(stat_path, function(stat_err, stat)
|
||||
cb(nil, link, stat)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@param entry_type nil|oil.EntryType
|
||||
---@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
|
||||
shortpath = util.addslash(shortpath)
|
||||
end
|
||||
return shortpath
|
||||
end
|
||||
|
||||
local file_columns = {}
|
||||
|
||||
local fs_stat_meta_fields = {
|
||||
stat = function(parent_url, entry, cb)
|
||||
local _, path = util.parse_url(parent_url)
|
||||
local dir = fs.posix_to_os_path(path)
|
||||
vim.loop.fs_stat(fs.join(dir, entry[FIELD.name]), cb)
|
||||
end,
|
||||
}
|
||||
|
||||
file_columns.size = {
|
||||
meta_fields = fs_stat_meta_fields,
|
||||
|
||||
render = function(entry, conf)
|
||||
local meta = entry[FIELD.meta]
|
||||
local stat = meta.stat
|
||||
if not stat then
|
||||
return ""
|
||||
end
|
||||
if stat.size >= 1e9 then
|
||||
return string.format("%.1fG", stat.size / 1e9)
|
||||
elseif stat.size >= 1e6 then
|
||||
return string.format("%.1fM", stat.size / 1e6)
|
||||
elseif stat.size >= 1e3 then
|
||||
return string.format("%.1fk", stat.size / 1e3)
|
||||
else
|
||||
return string.format("%d", stat.size)
|
||||
end
|
||||
end,
|
||||
|
||||
parse = function(line, conf)
|
||||
return line:match("^(%d+%S*)%s+(.*)$")
|
||||
end,
|
||||
}
|
||||
|
||||
-- TODO support file permissions on windows
|
||||
if not fs.is_windows then
|
||||
file_columns.permissions = {
|
||||
meta_fields = fs_stat_meta_fields,
|
||||
|
||||
render = function(entry, conf)
|
||||
local meta = entry[FIELD.meta]
|
||||
local stat = meta.stat
|
||||
if not stat then
|
||||
return ""
|
||||
end
|
||||
return permissions.mode_to_str(stat.mode)
|
||||
end,
|
||||
|
||||
parse = function(line, conf)
|
||||
return permissions.parse(line)
|
||||
end,
|
||||
|
||||
compare = function(entry, parsed_value)
|
||||
local meta = entry[FIELD.meta]
|
||||
if parsed_value and meta.stat and meta.stat.mode then
|
||||
local mask = bit.lshift(1, 12) - 1
|
||||
local old_mode = bit.band(meta.stat.mode, mask)
|
||||
if parsed_value ~= old_mode then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
|
||||
render_action = function(action)
|
||||
local _, path = util.parse_url(action.url)
|
||||
return string.format(
|
||||
"CHMOD %s %s",
|
||||
permissions.mode_to_octal_str(action.value),
|
||||
M.to_short_os_path(path, action.entry_type)
|
||||
)
|
||||
end,
|
||||
|
||||
perform_action = function(action, callback)
|
||||
local _, path = util.parse_url(action.url)
|
||||
path = fs.posix_to_os_path(path)
|
||||
vim.loop.fs_stat(path, function(err, stat)
|
||||
if err then
|
||||
return callback(err)
|
||||
end
|
||||
-- We are only changing the lower 12 bits of the mode
|
||||
local mask = bit.bnot(bit.lshift(1, 12) - 1)
|
||||
local old_mode = bit.band(stat.mode, mask)
|
||||
vim.loop.fs_chmod(path, bit.bor(old_mode, action.value), callback)
|
||||
end)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local current_year = vim.fn.strftime("%Y")
|
||||
|
||||
for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
|
||||
file_columns[time_key] = {
|
||||
meta_fields = fs_stat_meta_fields,
|
||||
|
||||
render = function(entry, conf)
|
||||
local meta = entry[FIELD.meta]
|
||||
local stat = meta.stat
|
||||
local fmt = conf and conf.format
|
||||
local ret
|
||||
if fmt then
|
||||
ret = vim.fn.strftime(fmt, stat[time_key].sec)
|
||||
else
|
||||
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)
|
||||
else
|
||||
ret = vim.fn.strftime("%b %d %H:%M", stat[time_key].sec)
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end,
|
||||
|
||||
parse = function(line, conf)
|
||||
local fmt = conf and conf.format
|
||||
local pattern
|
||||
if fmt then
|
||||
pattern = fmt:gsub("%%.", "%%S+")
|
||||
else
|
||||
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
|
||||
end
|
||||
return line:match("^(" .. pattern .. ")%s+(.+)$")
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return nil|oil.ColumnDefinition
|
||||
M.get_column = function(name)
|
||||
return file_columns[name]
|
||||
end
|
||||
|
||||
---@param url string
|
||||
---@param callback fun(url: string)
|
||||
M.normalize_url = function(url, callback)
|
||||
local scheme, path = util.parse_url(url)
|
||||
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p")
|
||||
local realpath = vim.loop.fs_realpath(os_path) or os_path
|
||||
local norm_path = util.addslash(fs.os_to_posix_path(realpath))
|
||||
if norm_path ~= os_path then
|
||||
callback(scheme .. fs.os_to_posix_path(norm_path))
|
||||
else
|
||||
callback(util.addslash(url))
|
||||
end
|
||||
end
|
||||
|
||||
---@param url string
|
||||
---@param column_defs string[]
|
||||
---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[])
|
||||
M.list = function(url, column_defs, callback)
|
||||
local _, path = util.parse_url(url)
|
||||
local dir = fs.posix_to_os_path(path)
|
||||
local fetch_meta = columns.get_metadata_fetcher(M, column_defs)
|
||||
cache.begin_update_url(url)
|
||||
local function cb(err, data)
|
||||
if err or not data then
|
||||
cache.end_update_url(url)
|
||||
end
|
||||
callback(err, data)
|
||||
end
|
||||
vim.loop.fs_opendir(dir, function(open_err, fd)
|
||||
if open_err then
|
||||
if open_err:match("^ENOENT: no such file or directory") then
|
||||
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse
|
||||
-- and edit a not-yet-existing directory.
|
||||
return cb()
|
||||
else
|
||||
return cb(open_err)
|
||||
end
|
||||
end
|
||||
local read_next
|
||||
read_next = function(read_err)
|
||||
if read_err then
|
||||
cb(read_err)
|
||||
return
|
||||
end
|
||||
vim.loop.fs_readdir(fd, function(err, entries)
|
||||
if err then
|
||||
vim.loop.fs_closedir(fd, function()
|
||||
cb(err)
|
||||
end)
|
||||
return
|
||||
elseif entries then
|
||||
local poll = util.cb_collect(#entries, function(inner_err)
|
||||
if inner_err then
|
||||
cb(inner_err)
|
||||
else
|
||||
cb(nil, true)
|
||||
read_next()
|
||||
end
|
||||
end)
|
||||
for _, entry in ipairs(entries) do
|
||||
local cache_entry = cache.create_entry(url, entry.name, entry.type)
|
||||
fetch_meta(url, cache_entry, function(meta_err)
|
||||
if err then
|
||||
poll(meta_err)
|
||||
else
|
||||
local meta = cache_entry[FIELD.meta]
|
||||
-- Make sure we always get fs_stat info for links
|
||||
if entry.type == "link" then
|
||||
read_link_data(fs.join(dir, entry.name), function(link_err, link, link_stat)
|
||||
if link_err then
|
||||
poll(link_err)
|
||||
else
|
||||
if not meta then
|
||||
meta = {}
|
||||
cache_entry[FIELD.meta] = meta
|
||||
end
|
||||
meta.link = link
|
||||
meta.link_stat = link_stat
|
||||
cache.store_entry(url, cache_entry)
|
||||
poll()
|
||||
end
|
||||
end)
|
||||
else
|
||||
cache.store_entry(url, cache_entry)
|
||||
poll()
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
else
|
||||
vim.loop.fs_closedir(fd, function(close_err)
|
||||
if close_err then
|
||||
cb(close_err)
|
||||
else
|
||||
cb()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
read_next()
|
||||
end, 100) -- TODO do some testing for this
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return boolean
|
||||
M.is_modifiable = function(bufnr)
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
local _, path = util.parse_url(bufname)
|
||||
local dir = fs.posix_to_os_path(path)
|
||||
local stat = vim.loop.fs_stat(dir)
|
||||
if not stat then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Can't do permissions checks on windows
|
||||
if fs.is_windows then
|
||||
return true
|
||||
end
|
||||
|
||||
local uid = vim.loop.getuid()
|
||||
local gid = vim.loop.getgid()
|
||||
local rwx
|
||||
if uid == stat.uid then
|
||||
rwx = bit.rshift(stat.mode, 6)
|
||||
elseif gid == stat.gid then
|
||||
rwx = bit.rshift(stat.mode, 3)
|
||||
else
|
||||
rwx = stat.mode
|
||||
end
|
||||
return bit.band(rwx, 2) ~= 0
|
||||
end
|
||||
|
||||
---@param url string
|
||||
M.url_to_buffer_name = function(url)
|
||||
local _, path = util.parse_url(url)
|
||||
return fs.posix_to_os_path(path)
|
||||
end
|
||||
|
||||
---@param action oil.Action
|
||||
---@return string
|
||||
M.render_action = function(action)
|
||||
if action.type == "create" then
|
||||
local _, path = util.parse_url(action.url)
|
||||
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)
|
||||
end
|
||||
return ret
|
||||
elseif action.type == "delete" then
|
||||
local _, path = util.parse_url(action.url)
|
||||
return string.format("DELETE %s", M.to_short_os_path(path, action.entry_type))
|
||||
elseif action.type == "move" or action.type == "copy" then
|
||||
local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
|
||||
if dest_adapter == M then
|
||||
local _, src_path = util.parse_url(action.src_url)
|
||||
local _, dest_path = util.parse_url(action.dest_url)
|
||||
return string.format(
|
||||
" %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)
|
||||
)
|
||||
else
|
||||
-- We should never hit this because we don't implement supports_xfer
|
||||
error("files adapter doesn't support cross-adapter move/copy")
|
||||
end
|
||||
else
|
||||
error(string.format("Bad action type: '%s'", action.type))
|
||||
end
|
||||
end
|
||||
|
||||
---@param action oil.Action
|
||||
---@param cb fun(err: nil|string)
|
||||
M.perform_action = function(action, cb)
|
||||
if action.type == "create" then
|
||||
local _, path = util.parse_url(action.url)
|
||||
path = fs.posix_to_os_path(path)
|
||||
if action.entry_type == "directory" then
|
||||
vim.loop.fs_mkdir(path, 493, function(err)
|
||||
-- Ignore if the directory already exists
|
||||
if not err or err:match("^EEXIST:") then
|
||||
cb()
|
||||
else
|
||||
cb(err)
|
||||
end
|
||||
end) -- 0755
|
||||
elseif action.entry_type == "link" and action.link then
|
||||
local flags = nil
|
||||
local target = fs.posix_to_os_path(action.link)
|
||||
if fs.is_windows then
|
||||
flags = {
|
||||
dir = vim.fn.isdirectory(target) == 1,
|
||||
junction = false,
|
||||
}
|
||||
end
|
||||
vim.loop.fs_symlink(target, path, flags, cb)
|
||||
else
|
||||
fs.touch(path, cb)
|
||||
end
|
||||
elseif action.type == "delete" then
|
||||
local _, path = util.parse_url(action.url)
|
||||
path = fs.posix_to_os_path(path)
|
||||
fs.recursive_delete(action.entry_type, path, cb)
|
||||
elseif action.type == "move" then
|
||||
local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
|
||||
if dest_adapter == M then
|
||||
local _, src_path = util.parse_url(action.src_url)
|
||||
local _, dest_path = util.parse_url(action.dest_url)
|
||||
src_path = fs.posix_to_os_path(src_path)
|
||||
dest_path = fs.posix_to_os_path(dest_path)
|
||||
fs.recursive_move(action.entry_type, src_path, dest_path, vim.schedule_wrap(cb))
|
||||
else
|
||||
-- We should never hit this because we don't implement supports_xfer
|
||||
cb("files adapter doesn't support cross-adapter move")
|
||||
end
|
||||
elseif action.type == "copy" then
|
||||
local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
|
||||
if dest_adapter == M then
|
||||
local _, src_path = util.parse_url(action.src_url)
|
||||
local _, dest_path = util.parse_url(action.dest_url)
|
||||
src_path = fs.posix_to_os_path(src_path)
|
||||
dest_path = fs.posix_to_os_path(dest_path)
|
||||
fs.recursive_copy(action.entry_type, src_path, dest_path, cb)
|
||||
else
|
||||
-- We should never hit this because we don't implement supports_xfer
|
||||
cb("files adapter doesn't support cross-adapter copy")
|
||||
end
|
||||
else
|
||||
cb(string.format("Bad action type: %s", action.type))
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
103
lua/oil/adapters/files/permissions.lua
Normal file
103
lua/oil/adapters/files/permissions.lua
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
local M = {}
|
||||
|
||||
---@param exe_modifier nil|string
|
||||
---@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 "-")
|
||||
if exe_modifier then
|
||||
if bit.band(num, 1) ~= 0 then
|
||||
return str .. exe_modifier
|
||||
else
|
||||
return str .. exe_modifier:upper()
|
||||
end
|
||||
else
|
||||
return str .. (bit.band(num, 1) ~= 0 and "x" or "-")
|
||||
end
|
||||
end
|
||||
|
||||
---@param mode integer
|
||||
---@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)
|
||||
end
|
||||
|
||||
---@param mode integer
|
||||
---@return string
|
||||
M.mode_to_octal_str = function(mode)
|
||||
local mask = 7
|
||||
return tostring(bit.band(mask, bit.rshift(mode, 9)))
|
||||
.. tostring(bit.band(mask, bit.rshift(mode, 6)))
|
||||
.. tostring(bit.band(mask, bit.rshift(mode, 3)))
|
||||
.. tostring(bit.band(mask, mode))
|
||||
end
|
||||
|
||||
---@param str string String of 3 characters
|
||||
---@return integer
|
||||
local function str_to_mode(str)
|
||||
local r, w, x = unpack(vim.split(str, ""))
|
||||
local mode = 0
|
||||
if r == "r" then
|
||||
mode = bit.bor(mode, 4)
|
||||
elseif r ~= "-" then
|
||||
return nil
|
||||
end
|
||||
if w == "w" then
|
||||
mode = bit.bor(mode, 2)
|
||||
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
|
||||
mode = bit.bor(mode, 1)
|
||||
elseif x ~= "-" and x ~= "T" and x ~= "S" then
|
||||
return nil
|
||||
end
|
||||
return mode
|
||||
end
|
||||
|
||||
---@param perm string
|
||||
---@return integer
|
||||
local function parse_extra_bits(perm)
|
||||
perm = perm:lower()
|
||||
local mode = 0
|
||||
if perm:sub(3, 3) == "s" then
|
||||
mode = bit.bor(mode, 4)
|
||||
end
|
||||
if perm:sub(6, 6) == "s" then
|
||||
mode = bit.bor(mode, 2)
|
||||
end
|
||||
if perm:sub(9, 9) == "t" then
|
||||
mode = bit.bor(mode, 1)
|
||||
end
|
||||
return mode
|
||||
end
|
||||
|
||||
---@param line string
|
||||
---@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*(.*)$")
|
||||
if not strval then
|
||||
return
|
||||
end
|
||||
local user_mode = str_to_mode(strval:sub(1, 3))
|
||||
local group_mode = str_to_mode(strval:sub(4, 6))
|
||||
local any_mode = str_to_mode(strval:sub(7, 9))
|
||||
local extra = parse_extra_bits(strval)
|
||||
if not user_mode or not group_mode or not any_mode then
|
||||
return
|
||||
end
|
||||
local mode = bit.bor(bit.lshift(user_mode, 6), bit.lshift(group_mode, 3))
|
||||
mode = bit.bor(mode, any_mode)
|
||||
mode = bit.bor(mode, bit.lshift(extra, 9))
|
||||
return mode, rem
|
||||
end
|
||||
|
||||
return M
|
||||
454
lua/oil/adapters/ssh.lua
Normal file
454
lua/oil/adapters/ssh.lua
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
local cache = require("oil.cache")
|
||||
local config = require("oil.config")
|
||||
local fs = require("oil.fs")
|
||||
local files = require("oil.adapters.files")
|
||||
local permissions = require("oil.adapters.files.permissions")
|
||||
local ssh_connection = require("oil.adapters.ssh.connection")
|
||||
local pathutil = require("oil.pathutil")
|
||||
local shell = require("oil.shell")
|
||||
local util = require("oil.util")
|
||||
local FIELD = require("oil.constants").FIELD
|
||||
local M = {}
|
||||
|
||||
---@class oil.sshUrl
|
||||
---@field scheme string
|
||||
---@field host string
|
||||
---@field user nil|string
|
||||
---@field port nil|integer
|
||||
---@field path string
|
||||
|
||||
---@param oil_url string
|
||||
---@return oil.sshUrl
|
||||
local function parse_url(oil_url)
|
||||
local scheme, url = util.parse_url(oil_url)
|
||||
local ret = { scheme = scheme }
|
||||
local username, rem = url:match("^([^@%s]+)@(.*)$")
|
||||
ret.user = username
|
||||
url = rem or url
|
||||
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("^([^/]+)/(.*)$")
|
||||
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))
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param url oil.sshUrl
|
||||
---@return string
|
||||
local function url_to_str(url)
|
||||
local pieces = { url.scheme }
|
||||
if url.user then
|
||||
table.insert(pieces, url.user)
|
||||
table.insert(pieces, "@")
|
||||
end
|
||||
table.insert(pieces, url.host)
|
||||
if url.port then
|
||||
table.insert(pieces, string.format(":%d", url.port))
|
||||
end
|
||||
table.insert(pieces, "/")
|
||||
table.insert(pieces, url.path)
|
||||
return table.concat(pieces, "")
|
||||
end
|
||||
|
||||
---@param url oil.sshUrl
|
||||
---@return string
|
||||
local function url_to_scp(url)
|
||||
local pieces = {}
|
||||
if url.user then
|
||||
table.insert(pieces, url.user)
|
||||
table.insert(pieces, "@")
|
||||
end
|
||||
table.insert(pieces, url.host)
|
||||
table.insert(pieces, ":")
|
||||
if url.port then
|
||||
table.insert(pieces, string.format(":%d", url.port))
|
||||
end
|
||||
table.insert(pieces, url.path)
|
||||
return table.concat(pieces, "")
|
||||
end
|
||||
|
||||
local _connections = {}
|
||||
---@param url string
|
||||
---@param allow_retry nil|boolean
|
||||
local function get_connection(url, allow_retry)
|
||||
local res = parse_url(url)
|
||||
res.scheme = config.adapters.ssh
|
||||
res.path = ""
|
||||
local key = url_to_str(res)
|
||||
local conn = _connections[key]
|
||||
if not conn or (allow_retry and conn.connection_error) then
|
||||
conn = ssh_connection.new(res)
|
||||
_connections[key] = conn
|
||||
end
|
||||
return conn
|
||||
end
|
||||
|
||||
local typechar_map = {
|
||||
l = "link",
|
||||
d = "directory",
|
||||
p = "fifo",
|
||||
s = "socket",
|
||||
["-"] = "file",
|
||||
}
|
||||
---@param line string
|
||||
---@return string Name of entry
|
||||
---@return oil.EntryType
|
||||
---@return nil|table Metadata for entry
|
||||
local function parse_ls_line(line)
|
||||
local typechar, perms, refcount, user, group, size, date, name =
|
||||
line:match("^(.)(%S+)%s+(%d+)%s+(%S+)%s+(%S+)%s+(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)$")
|
||||
if not typechar then
|
||||
error(string.format("Could not parse '%s'", line))
|
||||
end
|
||||
local type = typechar_map[typechar] or "file"
|
||||
|
||||
local meta = {
|
||||
user = user,
|
||||
group = group,
|
||||
mode = permissions.parse(perms),
|
||||
refcount = tonumber(refcount),
|
||||
size = tonumber(size),
|
||||
iso_modified_date = date,
|
||||
}
|
||||
if type == "link" then
|
||||
local link
|
||||
name, link = unpack(vim.split(name, " -> ", { plain = true }))
|
||||
if vim.endswith(link, "/") then
|
||||
link = link:sub(1, #link - 1)
|
||||
end
|
||||
meta.link = link
|
||||
end
|
||||
|
||||
return name, type, meta
|
||||
end
|
||||
|
||||
local ssh_columns = {}
|
||||
ssh_columns.permissions = {
|
||||
render = function(entry, conf)
|
||||
local meta = entry[FIELD.meta]
|
||||
return permissions.mode_to_str(meta.mode)
|
||||
end,
|
||||
|
||||
parse = function(line, conf)
|
||||
return permissions.parse(line)
|
||||
end,
|
||||
|
||||
compare = function(entry, parsed_value)
|
||||
local meta = entry[FIELD.meta]
|
||||
if parsed_value and meta.mode then
|
||||
local mask = bit.lshift(1, 12) - 1
|
||||
local old_mode = bit.band(meta.mode, mask)
|
||||
if parsed_value ~= old_mode then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
|
||||
render_action = function(action)
|
||||
return string.format("CHMOD %s %s", permissions.mode_to_octal_str(action.value), action.url)
|
||||
end,
|
||||
|
||||
perform_action = function(action, callback)
|
||||
local res = parse_url(action.url)
|
||||
local conn = get_connection(action.url)
|
||||
local octal = permissions.mode_to_octal_str(action.value)
|
||||
conn:run(string.format("chmod %s '%s'", octal, res.path), callback)
|
||||
end,
|
||||
}
|
||||
|
||||
ssh_columns.size = {
|
||||
render = function(entry, conf)
|
||||
local meta = entry[FIELD.meta]
|
||||
if meta.size >= 1e9 then
|
||||
return string.format("%.1fG", meta.size / 1e9)
|
||||
elseif meta.size >= 1e6 then
|
||||
return string.format("%.1fM", meta.size / 1e6)
|
||||
elseif meta.size >= 1e3 then
|
||||
return string.format("%.1fk", meta.size / 1e3)
|
||||
else
|
||||
return string.format("%d", meta.size)
|
||||
end
|
||||
end,
|
||||
|
||||
parse = function(line, conf)
|
||||
return line:match("^(%d+%S*)%s+(.*)$")
|
||||
end,
|
||||
}
|
||||
|
||||
---@param name string
|
||||
---@return nil|oil.ColumnDefinition
|
||||
M.get_column = function(name)
|
||||
return ssh_columns[name]
|
||||
end
|
||||
|
||||
---For debugging
|
||||
M.open_terminal = function()
|
||||
local conn = get_connection(vim.api.nvim_buf_get_name(0))
|
||||
if conn then
|
||||
conn:open_terminal()
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufname string
|
||||
---@return string
|
||||
M.get_parent = function(bufname)
|
||||
local res = parse_url(bufname)
|
||||
res.path = pathutil.parent(res.path)
|
||||
return url_to_str(res)
|
||||
end
|
||||
|
||||
---@param url string
|
||||
---@param callback fun(url: string)
|
||||
M.normalize_url = function(url, callback)
|
||||
local res = parse_url(url)
|
||||
local conn = get_connection(url, true)
|
||||
|
||||
local path = res.path
|
||||
if path == "" then
|
||||
path = "."
|
||||
end
|
||||
|
||||
local cmd = string.format(
|
||||
'if ! readlink -f "%s" 2>/dev/null; then [[ "%s" == /* ]] && echo "%s" || echo "$PWD/%s"; fi',
|
||||
path,
|
||||
path,
|
||||
path,
|
||||
path
|
||||
)
|
||||
conn:run(cmd, function(err, lines)
|
||||
if err then
|
||||
vim.notify(string.format("Error normalizing url %s: %s", url, err), vim.log.levels.WARN)
|
||||
return callback(url)
|
||||
end
|
||||
local abspath = table.concat(lines, "")
|
||||
if vim.endswith(abspath, ".") then
|
||||
abspath = abspath:sub(1, #abspath - 1)
|
||||
end
|
||||
abspath = util.addslash(abspath)
|
||||
if abspath == res.path then
|
||||
callback(url)
|
||||
else
|
||||
res.path = abspath
|
||||
callback(url_to_str(res))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local dir_meta = {}
|
||||
|
||||
---@param url string
|
||||
---@param column_defs string[]
|
||||
---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[])
|
||||
M.list = function(url, column_defs, callback)
|
||||
local res = parse_url(url)
|
||||
|
||||
local path_postfix = ""
|
||||
if res.path ~= "" then
|
||||
path_postfix = string.format(" '%s'", res.path)
|
||||
end
|
||||
local conn = get_connection(url)
|
||||
cache.begin_update_url(url)
|
||||
local function cb(err, data)
|
||||
if err or not data then
|
||||
cache.end_update_url(url)
|
||||
end
|
||||
callback(err, data)
|
||||
end
|
||||
conn:run("ls -fl" .. path_postfix, function(err, lines)
|
||||
if err 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 cb()
|
||||
else
|
||||
return cb(err)
|
||||
end
|
||||
end
|
||||
local any_links = false
|
||||
local entries = {}
|
||||
for _, line in ipairs(lines) do
|
||||
if line ~= "" and not line:match("^total") then
|
||||
local name, type, meta = parse_ls_line(line)
|
||||
if name == "." then
|
||||
dir_meta[url] = meta
|
||||
elseif name ~= ".." then
|
||||
if type == "link" then
|
||||
any_links = true
|
||||
end
|
||||
local cache_entry = cache.create_entry(url, name, type)
|
||||
entries[name] = cache_entry
|
||||
cache_entry[FIELD.meta] = meta
|
||||
cache.store_entry(url, cache_entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
if any_links then
|
||||
-- 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
|
||||
conn:run("ls -fLl" .. path_postfix, 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
|
||||
return cb(link_err)
|
||||
end
|
||||
for _, line in ipairs(link_lines) do
|
||||
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
|
||||
local cache_entry = entries[name]
|
||||
if cache_entry[FIELD.type] == "link" then
|
||||
cache_entry[FIELD.meta].link_stat = {
|
||||
type = type,
|
||||
size = meta.size,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
cb()
|
||||
end)
|
||||
else
|
||||
cb()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return boolean
|
||||
M.is_modifiable = function(bufnr)
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
local meta = dir_meta[bufname]
|
||||
if not meta then
|
||||
-- Directories that don't exist yet are modifiable
|
||||
return true
|
||||
end
|
||||
local conn = get_connection(bufname)
|
||||
if not conn.meta.user or not conn.meta.groups then
|
||||
return false
|
||||
end
|
||||
local rwx
|
||||
if meta.user == conn.meta.user then
|
||||
rwx = bit.rshift(meta.mode, 6)
|
||||
elseif vim.tbl_contains(conn.meta.groups, meta.group) then
|
||||
rwx = bit.rshift(meta.mode, 3)
|
||||
else
|
||||
rwx = meta.mode
|
||||
end
|
||||
return bit.band(rwx, 2) ~= 0
|
||||
end
|
||||
|
||||
---@param url string
|
||||
M.url_to_buffer_name = function(url)
|
||||
local _, rem = util.parse_url(url)
|
||||
-- Let netrw handle editing files
|
||||
return "scp://" .. rem
|
||||
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.link then
|
||||
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
|
||||
local src = action.src_url
|
||||
local dest = action.dest_url
|
||||
if config.get_adapter_by_scheme(src) == M then
|
||||
local _, path = util.parse_url(dest)
|
||||
dest = files.to_short_os_path(path, action.entry_type)
|
||||
else
|
||||
local _, path = util.parse_url(src)
|
||||
src = files.to_short_os_path(path, action.entry_type)
|
||||
end
|
||||
return string.format(" %s %s -> %s", action.type:upper(), src, dest)
|
||||
else
|
||||
error(string.format("Bad action type: '%s'", action.type))
|
||||
end
|
||||
end
|
||||
|
||||
---@param action oil.Action
|
||||
---@param cb fun(err: nil|string)
|
||||
M.perform_action = function(action, cb)
|
||||
if action.type == "create" then
|
||||
local res = parse_url(action.url)
|
||||
local conn = get_connection(action.url)
|
||||
if action.entry_type == "directory" then
|
||||
conn:run(string.format("mkdir -p '%s'", res.path), cb)
|
||||
elseif action.entry_type == "link" and action.link then
|
||||
conn:run(string.format("ln -s '%s' '%s'", action.link, res.path), cb)
|
||||
else
|
||||
conn:run(string.format("touch '%s'", res.path), cb)
|
||||
end
|
||||
elseif action.type == "delete" then
|
||||
local res = parse_url(action.url)
|
||||
local conn = get_connection(action.url)
|
||||
conn:run(string.format("rm -rf '%s'", res.path), cb)
|
||||
elseif action.type == "move" then
|
||||
local src_adapter = config.get_adapter_by_scheme(action.src_url)
|
||||
local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
|
||||
if src_adapter == M and dest_adapter == M then
|
||||
local src_res = parse_url(action.src_url)
|
||||
local dest_res = parse_url(action.dest_url)
|
||||
local src_conn = get_connection(action.src_url)
|
||||
local dest_conn = get_connection(action.dest_url)
|
||||
if src_conn ~= dest_conn then
|
||||
shell.run({ "scp", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err)
|
||||
if err then
|
||||
return cb(err)
|
||||
end
|
||||
src_conn:run(string.format("rm -rf '%s'", src_res.path), cb)
|
||||
end)
|
||||
else
|
||||
src_conn:run(string.format("mv '%s' '%s'", src_res.path, dest_res.path), cb)
|
||||
end
|
||||
else
|
||||
cb("We should never attempt to move across adapters")
|
||||
end
|
||||
elseif action.type == "copy" then
|
||||
local src_adapter = config.get_adapter_by_scheme(action.src_url)
|
||||
local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
|
||||
if src_adapter == M and dest_adapter == M then
|
||||
local src_res = parse_url(action.src_url)
|
||||
local dest_res = parse_url(action.dest_url)
|
||||
local src_conn = get_connection(action.src_url)
|
||||
local dest_conn = get_connection(action.dest_url)
|
||||
if src_conn.host ~= dest_conn.host then
|
||||
shell.run({ "scp", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb)
|
||||
end
|
||||
src_conn:run(string.format("cp -r '%s' '%s'", src_res.path, dest_res.path), cb)
|
||||
else
|
||||
local src_arg
|
||||
local dest_arg
|
||||
if src_adapter == M then
|
||||
src_arg = url_to_scp(parse_url(action.src_url))
|
||||
local _, path = util.parse_url(action.dest_url)
|
||||
dest_arg = fs.posix_to_os_path(path)
|
||||
else
|
||||
local _, path = util.parse_url(action.src_url)
|
||||
src_arg = fs.posix_to_os_path(path)
|
||||
dest_arg = url_to_scp(parse_url(action.dest_url))
|
||||
end
|
||||
shell.run({ "scp", "-r", src_arg, dest_arg }, cb)
|
||||
end
|
||||
else
|
||||
cb(string.format("Bad action type: %s", action.type))
|
||||
end
|
||||
end
|
||||
|
||||
M.supports_xfer = { files = true }
|
||||
|
||||
return M
|
||||
270
lua/oil/adapters/ssh/connection.lua
Normal file
270
lua/oil/adapters/ssh/connection.lua
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
local util = require("oil.util")
|
||||
local SSHConnection = {}
|
||||
|
||||
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", "")
|
||||
table.insert(agg, line)
|
||||
end
|
||||
else
|
||||
for i, v in ipairs(output) do
|
||||
v = v:gsub("\r", "")
|
||||
if i == 1 then
|
||||
agg[#agg] = agg[#agg] .. v
|
||||
else
|
||||
table.insert(agg, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
return start
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param num_lines integer
|
||||
---@return string[]
|
||||
local function get_last_lines(bufnr, num_lines)
|
||||
local end_line = vim.api.nvim_buf_line_count(bufnr)
|
||||
num_lines = math.min(num_lines, end_line)
|
||||
local lines = {}
|
||||
while end_line > 0 and #lines < num_lines do
|
||||
local need_lines = num_lines - #lines
|
||||
lines = vim.list_extend(
|
||||
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
|
||||
table.remove(lines)
|
||||
end
|
||||
end_line = end_line - need_lines
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param url oil.sshUrl
|
||||
function SSHConnection.new(url)
|
||||
local host = url.host
|
||||
if url.user then
|
||||
host = url.user .. "@" .. host
|
||||
end
|
||||
if url.port then
|
||||
host = string.format("%s:%d", host, url.port)
|
||||
end
|
||||
local command = {
|
||||
"ssh",
|
||||
host,
|
||||
"/bin/bash",
|
||||
"--norc",
|
||||
"-c",
|
||||
"echo '_make_newline_'; echo '===READY==='; exec /bin/bash --norc",
|
||||
}
|
||||
local self = setmetatable({
|
||||
host = host,
|
||||
meta = {},
|
||||
commands = {},
|
||||
connected = false,
|
||||
connection_error = nil,
|
||||
}, {
|
||||
__index = SSHConnection,
|
||||
})
|
||||
|
||||
self.term_bufnr = vim.api.nvim_create_buf(false, true)
|
||||
local term_id
|
||||
local mode = vim.api.nvim_get_mode().mode
|
||||
util.run_in_fullscreen_win(self.term_bufnr, function()
|
||||
term_id = vim.api.nvim_open_term(self.term_bufnr, {
|
||||
on_input = function(_, _, _, data)
|
||||
pcall(vim.api.nvim_chan_send, self.jid, data)
|
||||
end,
|
||||
})
|
||||
end)
|
||||
self.term_id = term_id
|
||||
vim.api.nvim_chan_send(term_id, string.format("ssh %s\r\n", host))
|
||||
util.hack_around_termopen_autocmd(mode)
|
||||
|
||||
-- If it takes more than 2 seconds to connect, pop open the terminal
|
||||
vim.defer_fn(function()
|
||||
if not self.connected and not self.connection_error then
|
||||
self:open_terminal()
|
||||
end
|
||||
end, 2000)
|
||||
self._stdout = {}
|
||||
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"))
|
||||
local new_i_start = output_extend(self._stdout, output)
|
||||
self:_handle_output(new_i_start)
|
||||
end,
|
||||
on_exit = function(j, 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")
|
||||
else
|
||||
self:_set_connection_error("Unknown SSH error")
|
||||
end
|
||||
end, 20)
|
||||
end,
|
||||
})
|
||||
local exe = command[1]
|
||||
if jid == 0 then
|
||||
self:_set_connection_error(string.format("Passed invalid arguments to '%s'", exe))
|
||||
elseif jid == -1 then
|
||||
self:_set_connection_error(string.format("'%s' is not executable", exe))
|
||||
else
|
||||
self.jid = jid
|
||||
end
|
||||
self:run("whoami", function(err, lines)
|
||||
if err then
|
||||
vim.notify(string.format("Error fetching ssh connection user: %s", err), vim.log.levels.WARN)
|
||||
else
|
||||
self.meta.user = vim.trim(table.concat(lines, ""))
|
||||
end
|
||||
end)
|
||||
self:run("groups", function(err, lines)
|
||||
if err then
|
||||
vim.notify(
|
||||
string.format("Error fetching ssh connection user groups: %s", err),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
else
|
||||
self.meta.groups = vim.split(table.concat(lines, ""), "%s+", { trimempty = true })
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@param err string
|
||||
function SSHConnection:_set_connection_error(err)
|
||||
if self.connection_error then
|
||||
return
|
||||
end
|
||||
self.connection_error = err
|
||||
local commands = self.commands
|
||||
self.commands = {}
|
||||
for _, cmd in ipairs(commands) do
|
||||
cmd.cb(err)
|
||||
end
|
||||
end
|
||||
|
||||
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 self.term_winid then
|
||||
if vim.api.nvim_win_is_valid(self.term_winid) then
|
||||
vim.api.nvim_win_close(self.term_winid, true)
|
||||
end
|
||||
self.term_winid = nil
|
||||
end
|
||||
self.connected = true
|
||||
self._stdout = util.tbl_slice(self._stdout, i + 1)
|
||||
self:_handle_output(1)
|
||||
self:_consume()
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
for i = start_i, #self._stdout - 1 do
|
||||
local line = self._stdout[i]
|
||||
if line:match("^===BEGIN===%s*$") then
|
||||
self._stdout = util.tbl_slice(self._stdout, i + 1)
|
||||
self:_handle_output(1)
|
||||
return
|
||||
end
|
||||
-- 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+)%)===")
|
||||
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
|
||||
cb(nil, output)
|
||||
else
|
||||
cb(exit_code .. ": " .. table.concat(output, "\n"), output)
|
||||
end
|
||||
table.remove(self.commands, 1)
|
||||
self:_handle_output(1)
|
||||
self:_consume()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
self:open_terminal()
|
||||
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")
|
||||
end
|
||||
end
|
||||
-- We have to defer this so the terminal buffer has time to update
|
||||
vim.defer_fn(check_last_line, 10)
|
||||
end
|
||||
|
||||
function SSHConnection:open_terminal()
|
||||
if self.term_winid and vim.api.nvim_win_is_valid(self.term_winid) then
|
||||
vim.api.nvim_set_current_win(self.term_winid)
|
||||
return
|
||||
end
|
||||
local min_width = 120
|
||||
local min_height = 20
|
||||
local total_height = util.get_editor_height()
|
||||
local width = math.min(min_width, vim.o.columns - 2)
|
||||
local height = math.min(min_height, total_height - 3)
|
||||
local row = math.floor((util.get_editor_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",
|
||||
width = width,
|
||||
height = height,
|
||||
row = row,
|
||||
col = col,
|
||||
style = "minimal",
|
||||
border = "rounded",
|
||||
})
|
||||
vim.cmd.startinsert()
|
||||
end
|
||||
|
||||
---@param command string
|
||||
---@param callback fun(err: nil|string, lines: nil|string[])
|
||||
function SSHConnection:run(command, callback)
|
||||
if self.connection_error then
|
||||
callback(self.connection_error)
|
||||
else
|
||||
table.insert(self.commands, { cmd = command, cb = callback })
|
||||
self:_consume()
|
||||
end
|
||||
end
|
||||
|
||||
function SSHConnection:_consume()
|
||||
if self.connected and not vim.tbl_isempty(self.commands) then
|
||||
local cmd = self.commands[1]
|
||||
if not cmd.running then
|
||||
cmd.running = true
|
||||
vim.api.nvim_chan_send(
|
||||
self.jid,
|
||||
-- HACK: Sleep briefly to help reduce stderr/stdout interleaving.
|
||||
-- I want to find a way to flush the stderr before the echo DONE, but haven't yet.
|
||||
-- This was causing issues when ls directory that doesn't exist (b/c ls prints error)
|
||||
'echo "===BEGIN==="; ' .. cmd.cmd .. '; CODE=$?; sleep .01; echo "===DONE($CODE)==="\r'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return SSHConnection
|
||||
62
lua/oil/adapters/test.lua
Normal file
62
lua/oil/adapters/test.lua
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
local cache = require("oil.cache")
|
||||
local M = {}
|
||||
|
||||
---@param path string
|
||||
---@param column_defs string[]
|
||||
---@param cb fun(err: nil|string, entries: nil|oil.InternalEntry[])
|
||||
M.list = function(url, column_defs, cb)
|
||||
cb(nil, cache.list_url(url))
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return nil|oil.ColumnDefinition
|
||||
M.get_column = function(name)
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@param entry_type oil.EntryType
|
||||
M.test_set = function(path, entry_type)
|
||||
local parent = vim.fn.fnamemodify(path, ":h")
|
||||
if parent ~= path then
|
||||
M.test_set(parent, "directory")
|
||||
end
|
||||
local url = "oil-test://" .. path
|
||||
if cache.get_entry_by_url(url) then
|
||||
-- Already exists
|
||||
return
|
||||
end
|
||||
local name = vim.fn.fnamemodify(path, ":t")
|
||||
cache.create_and_store_entry("oil-test://" .. parent, name, entry_type)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return boolean
|
||||
M.is_modifiable = function(bufnr)
|
||||
return true
|
||||
end
|
||||
|
||||
---@param url string
|
||||
M.url_to_buffer_name = function(url)
|
||||
error("Test adapter cannot open files")
|
||||
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)
|
||||
else
|
||||
error("Bad action type")
|
||||
end
|
||||
end
|
||||
|
||||
---@param action oil.Action
|
||||
---@param cb fun(err: nil|string)
|
||||
M.perform_action = function(action, cb)
|
||||
cb()
|
||||
end
|
||||
|
||||
return M
|
||||
Loading…
Add table
Add a link
Reference in a new issue