feat(config): add per-host/bucket extra args for SSH, S3, and FTP (#171)

* fix(view): reapply column highlights after paste and buffer edits

Problem: Column extmarks (icon color, per-character permissions) are
applied via `util.set_highlights` only during `render_buffer`. Neovim
does not duplicate extmarks on yank/paste, so lines inserted via `yyp`
or `p` render without any column highlights.

Solution: Store `col_width` and `col_align` in `session[bufnr]` after
each render. Add `M.reapply_highlights` which re-parses all buffer
lines, reconstructs the column chunk table, and re-applies extmarks via
`util.set_highlights`. Wire it to a `TextChanged` autocmd (guarded by
`_rendering[bufnr]` to skip oil's own `nvim_buf_set_lines` calls).

* fix(view): resolve LuaLS warnings in `reapply_highlights`

* feat(config): add per-host/bucket extra args for SSH, S3, and FTP

Problem: `extra_scp_args`, `extra_s3_args`, and `extra_curl_args` are
global — there's no way to pass adapter-specific args only to a single
host or bucket (e.g. `-O` for a Synology NAS that requires legacy SCP
protocol, or `--endpoint-url` for an R2 bucket).

Solution: add `ssh_hosts`, `s3_buckets`, and `ftp_hosts` config tables
that map exact hostnames/bucket names to per-target arg lists. Per-target
args are appended after the global args at call time. The `scp()` helper
in `ssh.lua` accepts a `hosts` list so cross-host copies deduplicate
host lookups. `create_s3_command` in `s3fs.lua` extracts the bucket from
the command args with no call-site changes needed. `resolved_curl_args`
in `ftp.lua` is called by both `curl()` and `ftpcmd()`.

Based on: stevearc/oil.nvim#607
This commit is contained in:
Barrett Ruth 2026-03-18 12:40:32 -04:00 committed by GitHub
parent f988996059
commit e387a57c0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 227 additions and 22 deletions

View file

@ -21,9 +21,21 @@ local FIELD_META = constants.FIELD_META
---@field port nil|integer
---@field path string
---@param hosts string[]
---@param args string[]
local function scp(args, ...)
local cmd = vim.list_extend({ 'scp', '-C' }, config.extra_scp_args)
local function scp(hosts, args, ...)
local extra = vim.deepcopy(config.extra_scp_args)
local seen = {}
for _, host in ipairs(hosts) do
if not seen[host] then
seen[host] = true
local host_cfg = config.ssh_hosts[host]
if host_cfg and host_cfg.extra_scp_args then
vim.list_extend(extra, host_cfg.extra_scp_args)
end
end
end
local cmd = vim.list_extend({ 'scp', '-C' }, extra)
vim.list_extend(cmd, args)
shell.run(cmd, ...)
end
@ -320,12 +332,16 @@ 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)
if err then
return cb(err)
scp(
{ src_res.host, dest_res.host },
{ '-r', url_to_scp(src_res), url_to_scp(dest_res) },
function(err)
if err then
return cb(err)
end
src_conn:rm(src_res.path, cb)
end
src_conn:rm(src_res.path, cb)
end)
)
else
src_conn:mv(src_res.path, dest_res.path, cb)
end
@ -339,26 +355,31 @@ M.perform_action = function(action, cb)
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(
{ src_res.host, dest_res.host },
{ '-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)
end
else
local src_arg
local dest_arg
if src_adapter == M then
src_arg = url_to_scp(M.parse_url(action.src_url))
local src_res = M.parse_url(action.src_url)
local src_arg = url_to_scp(src_res)
local _, path = util.parse_url(action.dest_url)
assert(path)
dest_arg = fs.posix_to_os_path(path)
local dest_arg = fs.posix_to_os_path(path)
scp({ src_res.host }, { '-r', src_arg, dest_arg }, cb)
else
local _, path = util.parse_url(action.src_url)
assert(path)
src_arg = fs.posix_to_os_path(path)
dest_arg = url_to_scp(M.parse_url(action.dest_url))
local src_arg = fs.posix_to_os_path(path)
local dest_res = M.parse_url(action.dest_url)
local dest_arg = url_to_scp(dest_res)
scp({ dest_res.host }, { '-r', src_arg, dest_arg }, cb)
end
scp({ '-r', src_arg, dest_arg }, cb)
end
else
cb(string.format('Bad action type: %s', action.type))
@ -384,7 +405,7 @@ M.read_file = function(bufnr)
end
local tmp_bufnr = vim.fn.bufadd(tmpfile)
scp({ scp_url, tmpfile }, function(err)
scp({ url.host }, { scp_url, tmpfile }, function(err)
loading.set_loading(bufnr, false)
vim.bo[bufnr].modifiable = true
vim.cmd.doautocmd({ args = { 'BufReadPre', bufname }, mods = { silent = true } })
@ -426,7 +447,7 @@ M.write_file = function(bufnr)
vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } })
local tmp_bufnr = vim.fn.bufadd(tmpfile)
scp({ tmpfile, scp_url }, function(err)
scp({ url.host }, { 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)