feat: builtin support for editing files over ssh (#27)

This commit is contained in:
Steven Arcangeli 2023-01-13 23:28:24 -08:00
parent 75b710e311
commit ca4da68aae
18 changed files with 593 additions and 291 deletions

View file

@ -175,13 +175,20 @@ end
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
vim.loop.fs_realpath(os_path, function(err, new_os_path)
local realpath = new_os_path or os_path
vim.loop.fs_stat(
realpath,
vim.schedule_wrap(function(stat_err, stat)
if not stat or stat.type == "directory" then
local norm_path = util.addslash(fs.os_to_posix_path(realpath))
callback(scheme .. norm_path)
else
callback(realpath)
end
end)
)
end)
end
---@param url string
@ -303,12 +310,6 @@ M.is_modifiable = function(bufnr)
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)

View file

@ -2,8 +2,9 @@ local cache = require("oil.cache")
local config = require("oil.config")
local fs = require("oil.fs")
local files = require("oil.adapters.files")
local loading = require("oil.loading")
local permissions = require("oil.adapters.files.permissions")
local ssh_connection = require("oil.adapters.ssh.connection")
local sshfs = require("oil.adapters.ssh.sshfs")
local pathutil = require("oil.pathutil")
local shell = require("oil.shell")
local util = require("oil.util")
@ -85,62 +86,13 @@ local function get_connection(url, allow_retry)
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)
if not conn or (allow_retry and conn:get_connection_error()) then
conn = sshfs.new(res)
_connections[key] = conn
end
return conn
end
local typechar_map = {
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
---@return oil.EntryType
---@return nil|table Metadata for entry
local function parse_ls_line(line)
local typechar, perms, refcount, user, group, rem =
line:match("^(.)(%S+)%s+(%d+)%s+(%S+)%s+(%S+)%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),
}
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+(.*)")
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+(.*)")
meta.size = tonumber(size)
end
meta.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)
@ -171,8 +123,7 @@ ssh_columns.permissions = {
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)
conn:chmod(action.value, res.path, callback)
end,
}
@ -230,24 +181,9 @@ M.normalize_url = function(url, callback)
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)
conn:realpath(path, function(err, abspath)
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
@ -256,81 +192,19 @@ M.normalize_url = function(url, callback)
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)
local conn = get_connection(url)
conn:list_dir(url, res.path, function(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
@ -338,33 +212,27 @@ end
---@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
local conn = get_connection(bufname)
local dir_meta = conn:get_dir_meta(bufname)
if not dir_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
local meta = conn:get_meta()
if not meta.user or not 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)
if dir_meta.user == meta.user then
rwx = bit.rshift(dir_meta.mode, 6)
elseif vim.tbl_contains(meta.groups, dir_meta.group) then
rwx = bit.rshift(dir_meta.mode, 3)
else
rwx = meta.mode
rwx = dir_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)
@ -399,16 +267,16 @@ M.perform_action = function(action, cb)
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)
conn:mkdir(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)
conn:mklink(res.path, action.link, cb)
else
conn:run(string.format("touch '%s'", res.path), cb)
conn:touch(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)
conn:rm(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)
@ -418,14 +286,14 @@ 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
shell.run({ "scp", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err)
shell.run({ "scp", "-C", "-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)
src_conn:rm(src_res.path, cb)
end)
else
src_conn:run(string.format("mv '%s' '%s'", src_res.path, dest_res.path), cb)
src_conn:mv(src_res.path, dest_res.path, cb)
end
else
cb("We should never attempt to move across adapters")
@ -439,9 +307,9 @@ 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.host ~= dest_conn.host then
shell.run({ "scp", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb)
shell.run({ "scp", "-C", "-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)
src_conn:cp(src_res.path, dest_res.path, cb)
else
local src_arg
local dest_arg
@ -454,7 +322,7 @@ M.perform_action = function(action, cb)
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)
shell.run({ "scp", "-C", "-r", src_arg, dest_arg }, cb)
end
else
cb(string.format("Bad action type: %s", action.type))
@ -463,4 +331,63 @@ end
M.supports_xfer = { files = true }
---@param bufnr integer
M.read_file = function(bufnr)
loading.set_loading(bufnr, true)
local bufname = vim.api.nvim_buf_get_name(bufnr)
local url = parse_url(bufname)
local scp_url = url_to_scp(url)
local basename = pathutil.basename(bufname)
local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil")
fs.mkdirp(tmpdir)
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXX"))
vim.loop.fs_close(fd)
local tmp_bufnr = vim.fn.bufadd(tmpfile)
shell.run({ "scp", "-C", scp_url, tmpfile }, function(err)
loading.set_loading(bufnr, false)
vim.bo[bufnr].modifiable = 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"))
else
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {})
vim.api.nvim_buf_call(bufnr, function()
vim.cmd.read({ args = { tmpfile }, mods = { silent = true } })
end)
vim.loop.fs_unlink(tmpfile)
vim.api.nvim_buf_set_lines(bufnr, 0, 1, true, {})
end
vim.bo[bufnr].modified = false
vim.bo[bufnr].filetype = vim.filetype.match({ buf = bufnr, filename = basename })
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } })
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
end)
end
---@param bufnr integer
M.write_file = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr)
vim.bo[bufnr].modifiable = false
local url = parse_url(bufname)
local scp_url = url_to_scp(url)
local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil")
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXXXX"))
vim.loop.fs_close(fd)
vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } })
vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true } })
local tmp_bufnr = vim.fn.bufadd(tmpfile)
shell.run({ "scp", "-C", tmpfile, scp_url }, function(err)
if err then
vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR)
end
vim.bo[bufnr].modifiable = true
vim.bo[bufnr].modified = false
vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } })
vim.loop.fs_unlink(tmpfile)
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
end)
end
return M

View file

@ -0,0 +1,228 @@
local cache = require("oil.cache")
local permissions = require("oil.adapters.files.permissions")
local SSHConnection = require("oil.adapters.ssh.connection")
local util = require("oil.util")
local FIELD = require("oil.constants").FIELD
local SSHFS = {}
local typechar_map = {
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
---@return oil.EntryType
---@return nil|table Metadata for entry
local function parse_ls_line(line)
local typechar, perms, refcount, user, group, rem =
line:match("^(.)(%S+)%s+(%d+)%s+(%S+)%s+(%S+)%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),
}
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+(.*)")
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+(.*)")
meta.size = tonumber(size)
end
meta.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
---@param url oil.sshUrl
function SSHFS.new(url)
return setmetatable({
conn = SSHConnection.new(url),
}, {
__index = SSHFS,
})
end
function SSHFS:get_connection_error()
return self.conn.connection_error
end
---@param value integer
---@param path string
---@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, path), callback)
end
function SSHFS:open_terminal()
self.conn:open_terminal()
end
function SSHFS:realpath(path, callback)
local cmd = string.format(
'if ! readlink -f "%s" 2>/dev/null; then [[ "%s" == /* ]] && echo "%s" || echo "$PWD/%s"; fi',
path,
path,
path,
path
)
self.conn:run(cmd, function(err, lines)
if err then
return callback(err)
end
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
abspath = abspath:sub(1, #abspath - 1)
end
self.conn:run(string.format("ls -fld '%s'", 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"
else
local _
_, type = parse_ls_line(ls_lines[1])
end
if type == "directory" then
abspath = util.addslash(abspath)
end
callback(nil, abspath)
end)
end)
end
local dir_meta = {}
function SSHFS:list_dir(url, path, callback)
local path_postfix = ""
if path ~= "" then
path_postfix = string.format(" '%s'", path)
end
self.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 callback()
else
return callback(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
self.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 callback(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
callback()
end)
else
callback()
end
end)
end
---@param path string
---@param callback fun(err: nil|string)
function SSHFS:mkdir(path, callback)
self.conn:run(string.format("mkdir -p '%s'", path), callback)
end
---@param path string
---@param callback fun(err: nil|string)
function SSHFS:touch(path, callback)
self.conn:run(string.format("touch '%s'", 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'", link, 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'", 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'", src, 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'", src, dest), callback)
end
function SSHFS:get_dir_meta(url)
return dir_meta[url]
end
function SSHFS:get_meta()
return self.conn.meta
end
return SSHFS

View file

@ -1,6 +1,12 @@
local cache = require("oil.cache")
local M = {}
---@param url string
---@param callback fun(url: string)
M.normalize_url = function(url, callback)
callback(url)
end
---@param path string
---@param column_defs string[]
---@param cb fun(err: nil|string, entries: nil|oil.InternalEntry[])
@ -36,11 +42,6 @@ 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)
@ -59,4 +60,14 @@ M.perform_action = function(action, cb)
cb()
end
---@param bufnr integer
M.read_file = function(bufnr)
-- pass
end
---@param bufnr integer
M.write_file = function(bufnr)
-- pass
end
return M

View file

@ -67,12 +67,11 @@ local default_config = {
-- reason, I'm taking them out of the section above so they won't show up in the autogen docs.
default_config.adapters = {
["oil://"] = "files",
["oil-ssh://"] = "ssh",
["scp://"] = "ssh",
}
-- When opening the parent of a file, substitute these url schemes
default_config.remap_schemes = {
["scp://"] = "oil-ssh://",
["sftp://"] = "oil-ssh://",
-- For backwards compatibility
default_config.adapter_aliases = {
["oil-ssh://"] = "scp://",
}
local M = {}

View file

@ -14,13 +14,14 @@ local M = {}
---@field name string
---@field list fun(path: string, cb: fun(err: nil|string, entries: nil|oil.InternalEntry[]))
---@field is_modifiable fun(bufnr: integer): boolean
---@field url_to_buffer_name fun(url: string): string
---@field get_column fun(name: string): nil|oil.ColumnDefinition
---@field normalize_url nil|fun(url: string, callback: fun(url: string))
---@field normalize_url fun(url: string, callback: fun(url: string))
---@field get_parent nil|fun(bufname: string): string
---@field supports_xfer nil|table<string, boolean>
---@field render_action nil|fun(action: oil.Action): string
---@field perform_action nil|fun(action: oil.Action, cb: fun(err: nil|string))
---@field read_file fun(bufnr: integer)
---@field write_file fun(bufnr: integer)
---Get the entry on a specific line (1-indexed)
---@param bufnr integer
@ -189,7 +190,6 @@ M.get_buffer_parent_url = function(bufname)
local parent_url = util.addslash(scheme .. parent)
return parent_url, basename
else
scheme = config.remap_schemes[scheme] or scheme
local adapter = config.get_adapter_by_scheme(scheme)
local parent_url
if adapter and adapter.get_parent then
@ -370,7 +370,6 @@ M.select = function(opts)
local scheme, dir = util.parse_url(bufname)
local child = dir .. entry.name
local url = scheme .. child
local buffer_name
if
entry.type == "directory"
or (
@ -380,7 +379,6 @@ M.select = function(opts)
and entry.meta.link_stat.type == "directory"
)
then
buffer_name = util.addslash(url)
-- If this is a new directory BUT we think we already have an entry with this name, disallow
-- entry. This prevents the case of MOVE /foo -> /bar + CREATE /foo.
-- If you enter the new /foo, it will show the contents of the old /foo.
@ -392,7 +390,6 @@ M.select = function(opts)
if util.is_floating_win() then
vim.api.nvim_win_close(0, false)
end
buffer_name = adapter.url_to_buffer_name(url)
end
local mods = {
vertical = opts.vertical,
@ -405,7 +402,7 @@ M.select = function(opts)
local cmd = opts.split and "split" or "edit"
vim.cmd({
cmd = cmd,
args = { buffer_name },
args = { url },
mods = mods,
})
if opts.preview then
@ -514,19 +511,69 @@ M.save = function(opts)
mutator.try_write_changes(opts.confirm)
end
local function restore_alt_buf()
local config = require("oil.config")
local view = require("oil.view")
if vim.bo.filetype == "oil" then
view.set_win_options()
vim.api.nvim_win_set_var(0, "oil_did_enter", true)
elseif vim.w.oil_did_enter then
vim.api.nvim_win_del_var(0, "oil_did_enter")
-- We are entering a non-oil buffer *after* having been in an oil buffer
local has_orig, orig_buffer = pcall(vim.api.nvim_win_get_var, 0, "oil_original_buffer")
if has_orig and vim.api.nvim_buf_is_valid(orig_buffer) then
if vim.api.nvim_get_current_buf() ~= orig_buffer then
-- If we are editing a new file after navigating around oil, set the alternate buffer
-- to be the last buffer we were in before opening oil
vim.fn.setreg("#", orig_buffer)
else
-- If we are editing the same buffer that we started oil from, set the alternate to be
-- what it was before we opened oil
local has_orig_alt, alt_buffer =
pcall(vim.api.nvim_win_get_var, 0, "oil_original_alternate")
if has_orig_alt and vim.api.nvim_buf_is_valid(alt_buffer) then
vim.fn.setreg("#", alt_buffer)
end
end
end
if config.restore_win_options then
view.restore_win_options()
end
end
end
---@param bufnr integer
local function load_oil_buffer(bufnr)
local config = require("oil.config")
local keymap_util = require("oil.keymap_util")
local loading = require("oil.loading")
local util = require("oil.util")
local view = require("oil.view")
local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = config.get_adapter_by_scheme(bufname)
vim.bo[bufnr].buftype = "acwrite"
vim.bo[bufnr].filetype = "oil"
vim.bo[bufnr].bufhidden = "hide"
vim.bo[bufnr].syntax = "oil"
local scheme, path = util.parse_url(bufname)
if config.adapter_aliases[scheme] then
if scheme == "oil-ssh://" then
vim.notify_once(
'The "oil-ssh://" url scheme is deprecated, use "scp://" instead.\nSupport will be removed on 2023-06-01.',
vim.log.levels.WARN
)
end
scheme = config.adapter_aliases[scheme]
bufname = scheme .. path
util.rename_buffer(bufnr, bufname)
end
local adapter = config.get_adapter_by_scheme(scheme)
if vim.endswith(bufname, "/") then
-- This is a small quality-of-life thing. If the buffer name ends with a `/`, we know it's a
-- directory, and can set the filetype early. This is helpful for adapters with a lot of latency
-- (e.g. ssh) because it will set up the filetype keybinds at the *beginning* of the loading
-- process.
vim.bo[bufnr].filetype = "oil"
keymap_util.set_keymaps("", config.keymaps, bufnr)
end
loading.set_loading(bufnr, true)
local function finish(new_url)
if new_url ~= bufname then
@ -535,23 +582,31 @@ local function load_oil_buffer(bufnr)
-- have BufReadCmd called for it
return
end
bufname = new_url
end
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { emsg_silent = true } })
view.initialize(bufnr)
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { emsg_silent = true } })
if vim.endswith(bufname, "/") then
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { emsg_silent = true } })
view.initialize(bufnr)
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { emsg_silent = true } })
else
vim.bo[bufnr].buftype = "acwrite"
adapter.read_file(bufnr)
end
restore_alt_buf()
end
if adapter.normalize_url then
adapter.normalize_url(bufname, finish)
else
finish(util.addslash(bufname))
end
adapter.normalize_url(bufname, finish)
end
---Initialize oil
---@param opts nil|table
M.setup = function(opts)
local config = require("oil.config")
-- Disable netrw
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
config.setup(opts)
set_colors()
vim.api.nvim_create_user_command("Oil", function(args)
@ -574,6 +629,9 @@ M.setup = function(opts)
for scheme in pairs(config.adapters) do
table.insert(patterns, scheme .. "*")
end
for scheme in pairs(config.adapter_aliases) do
table.insert(patterns, scheme .. "*")
end
local scheme_pattern = table.concat(patterns, ",")
vim.api.nvim_create_autocmd("ColorScheme", {
@ -595,10 +653,16 @@ M.setup = function(opts)
pattern = scheme_pattern,
nested = true,
callback = function(params)
vim.cmd.doautocmd({ args = { "BufWritePre", params.file }, mods = { silent = true } })
M.save()
vim.bo[params.buf].modified = false
vim.cmd.doautocmd({ args = { "BufWritePost", params.file }, mods = { silent = true } })
local bufname = vim.api.nvim_buf_get_name(params.buf)
if vim.endswith(bufname, "/") then
vim.cmd.doautocmd({ args = { "BufWritePre", params.file }, mods = { silent = true } })
M.save()
vim.bo[params.buf].modified = false
vim.cmd.doautocmd({ args = { "BufWritePost", params.file }, mods = { silent = true } })
else
local adapter = config.get_adapter_by_scheme(bufname)
adapter.write_file(params.buf)
end
end,
})
vim.api.nvim_create_autocmd("BufWinLeave", {
@ -617,34 +681,15 @@ M.setup = function(opts)
group = aug,
pattern = "*",
callback = function()
local util = require("oil.util")
local view = require("oil.view")
if vim.bo.filetype == "oil" then
view.set_win_options()
vim.api.nvim_win_set_var(0, "oil_did_enter", true)
local scheme = util.parse_url(vim.api.nvim_buf_get_name(0))
if scheme and config.adapters[scheme] then
view.maybe_set_cursor()
elseif vim.w.oil_did_enter then
vim.api.nvim_win_del_var(0, "oil_did_enter")
-- We are entering a non-oil buffer *after* having been in an oil buffer
local has_orig, orig_buffer = pcall(vim.api.nvim_win_get_var, 0, "oil_original_buffer")
if has_orig and vim.api.nvim_buf_is_valid(orig_buffer) then
if vim.api.nvim_get_current_buf() ~= orig_buffer then
-- If we are editing a new file after navigating around oil, set the alternate buffer
-- to be the last buffer we were in before opening oil
vim.fn.setreg("#", orig_buffer)
else
-- If we are editing the same buffer that we started oil from, set the alternate to be
-- what it was before we opened oil
local has_orig_alt, alt_buffer =
pcall(vim.api.nvim_win_get_var, 0, "oil_original_alternate")
if has_orig_alt and vim.api.nvim_buf_is_valid(alt_buffer) then
vim.fn.setreg("#", alt_buffer)
end
end
end
if config.restore_win_options then
view.restore_win_options()
end
else
-- Only run this logic if we are *not* in an oil buffer.
-- Oil buffers have to run it in BufReadCmd after confirming they are a directory or a file
restore_alt_buf()
end
end,
})
@ -722,6 +767,7 @@ M.setup = function(opts)
end
end,
})
maybe_hijack_directory_buffer(0)
end

View file

@ -487,6 +487,7 @@ M.try_write_changes = function(confirm)
end
local actions = M.create_actions_from_diffs(all_diffs)
-- TODO(2023-06-01) If no one has reported data loss by this time, we can remove the disclaimer
disclaimer.show(function(disclaimed)
if not disclaimed then
return unlock()

View file

@ -1,29 +1,36 @@
local M = {}
M.run = function(cmd, callback)
M.run = function(cmd, opts, callback)
if not callback then
callback = opts
opts = {}
end
local stdout
local stderr = {}
local jid = vim.fn.jobstart(cmd, {
stdout_buffered = true,
stderr_buffered = true,
on_stdout = function(j, output)
stdout = output
end,
on_stderr = function(j, output)
stderr = output
end,
on_exit = vim.schedule_wrap(function(j, code)
if code == 0 then
callback(nil, stdout)
else
local err = table.concat(stderr, "\n")
if err == "" then
err = "Unknown error"
local jid = vim.fn.jobstart(
cmd,
vim.tbl_deep_extend("keep", opts, {
stdout_buffered = true,
stderr_buffered = true,
on_stdout = function(j, output)
stdout = output
end,
on_stderr = function(j, output)
stderr = output
end,
on_exit = vim.schedule_wrap(function(j, code)
if code == 0 then
callback(nil, stdout)
else
local err = table.concat(stderr, "\n")
if err == "" then
err = "Unknown error"
end
callback(err)
end
callback(err)
end
end),
})
end),
})
)
local exe
if type(cmd) == "string" then
exe = vim.split(cmd, "%s+")[1]

View file

@ -98,11 +98,10 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
end
local bufname = vim.api.nvim_buf_get_name(src_bufnr)
local scheme = M.parse_url(bufname)
-- If this buffer has a scheme (is not literally a file on disk), then we can use the simple
-- If this buffer is not literally a file on disk, then we can use the simple
-- rename logic. The only reason we can't use nvim_buf_set_name on files is because vim will
-- think that the new buffer conflicts with the file next time it tries to save.
if scheme or vim.fn.isdirectory(bufname) == 1 then
if not vim.loop.fs_stat(dest_buf_name) then
-- This will fail if the dest buf name already exists
local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name)
if ok then
@ -159,19 +158,10 @@ end
local function get_possible_buffer_names_from_url(url)
local fs = require("oil.fs")
local scheme, path = M.parse_url(url)
local ret = {}
for k, v in pairs(config.remap_schemes) do
if v == scheme then
if k ~= "default" then
table.insert(ret, k .. path)
end
end
end
if vim.tbl_isempty(ret) then
if config.adapters[scheme] == "files" then
return { fs.posix_to_os_path(path) }
else
return ret
end
return { url }
end
---@param entry_type oil.EntryType

View file

@ -178,6 +178,12 @@ M.initialize = function(bufnr)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
vim.bo[bufnr].buftype = "acwrite"
vim.bo[bufnr].syntax = "oil"
vim.bo[bufnr].filetype = "oil"
session[bufnr] = true
for k, v in pairs(config.buf_options) do
vim.api.nvim_buf_set_option(bufnr, k, v)