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

@ -37,6 +37,7 @@ jobs:
include: include:
- nvim_tag: v0.8.0 - nvim_tag: v0.8.0
- nvim_tag: v0.8.1 - nvim_tag: v0.8.1
- nvim_tag: v0.8.2
name: Run tests name: Run tests
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04

View file

@ -184,12 +184,10 @@ Note that file operations work _across adapters_. This means that you can use oi
This adapter allows you to browse files over ssh, much like netrw. To use it, simply open a buffer using the following name template: This adapter allows you to browse files over ssh, much like netrw. To use it, simply open a buffer using the following name template:
``` ```
nvim oil-ssh://[username@]hostname[:port]/[path] nvim scp://[username@]hostname[:port]/[path]
``` ```
This should look familiar. In fact, if you replace `oil-ssh://` with `sftp://`, this is the exact same url format that netrw uses. This may look familiar. In fact, this is the exact same url format that netrw uses.
While this adapter effectively replaces netrw for directory browsing, it still relies on netrw for file editing. When you open a file from oil, it will use the `scp://host/path/to/file.txt` format that triggers remote editing via netrw.
Note that at the moment the ssh adapter does not support Windows machines, and it requires the server to have a `/bin/bash` binary as well as standard unix commands (`rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`). Note that at the moment the ssh adapter does not support Windows machines, and it requires the server to have a `/bin/bash` binary as well as standard unix commands (`rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`).

View file

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

View file

@ -2,8 +2,9 @@ local cache = require("oil.cache")
local config = require("oil.config") local config = require("oil.config")
local fs = require("oil.fs") local fs = require("oil.fs")
local files = require("oil.adapters.files") local files = require("oil.adapters.files")
local loading = require("oil.loading")
local permissions = require("oil.adapters.files.permissions") 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 pathutil = require("oil.pathutil")
local shell = require("oil.shell") local shell = require("oil.shell")
local util = require("oil.util") local util = require("oil.util")
@ -85,62 +86,13 @@ local function get_connection(url, allow_retry)
res.path = "" res.path = ""
local key = url_to_str(res) local key = url_to_str(res)
local conn = _connections[key] local conn = _connections[key]
if not conn or (allow_retry and conn.connection_error) then if not conn or (allow_retry and conn:get_connection_error()) then
conn = ssh_connection.new(res) conn = sshfs.new(res)
_connections[key] = conn _connections[key] = conn
end end
return conn return conn
end 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 = {} local ssh_columns = {}
ssh_columns.permissions = { ssh_columns.permissions = {
render = function(entry, conf) render = function(entry, conf)
@ -171,8 +123,7 @@ ssh_columns.permissions = {
perform_action = function(action, callback) perform_action = function(action, callback)
local res = parse_url(action.url) local res = parse_url(action.url)
local conn = get_connection(action.url) local conn = get_connection(action.url)
local octal = permissions.mode_to_octal_str(action.value) conn:chmod(action.value, res.path, callback)
conn:run(string.format("chmod %s '%s'", octal, res.path), callback)
end, end,
} }
@ -230,24 +181,9 @@ M.normalize_url = function(url, callback)
path = "." path = "."
end end
local cmd = string.format( conn:realpath(path, function(err, abspath)
'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 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)
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) callback(url)
else else
res.path = abspath res.path = abspath
@ -256,81 +192,19 @@ M.normalize_url = function(url, callback)
end) end)
end end
local dir_meta = {}
---@param url string ---@param url string
---@param column_defs string[] ---@param column_defs string[]
---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[]) ---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[])
M.list = function(url, column_defs, callback) M.list = function(url, column_defs, callback)
local res = parse_url(url) 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) 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 if err or not data then
cache.end_update_url(url) cache.end_update_url(url)
end end
callback(err, data) 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)
end end
@ -338,33 +212,27 @@ end
---@return boolean ---@return boolean
M.is_modifiable = function(bufnr) M.is_modifiable = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local meta = dir_meta[bufname] local conn = get_connection(bufname)
if not meta then local dir_meta = conn:get_dir_meta(bufname)
if not dir_meta then
-- Directories that don't exist yet are modifiable -- Directories that don't exist yet are modifiable
return true return true
end end
local conn = get_connection(bufname) local meta = conn:get_meta()
if not conn.meta.user or not conn.meta.groups then if not meta.user or not meta.groups then
return false return false
end end
local rwx local rwx
if meta.user == conn.meta.user then if dir_meta.user == meta.user then
rwx = bit.rshift(meta.mode, 6) rwx = bit.rshift(dir_meta.mode, 6)
elseif vim.tbl_contains(conn.meta.groups, meta.group) then elseif vim.tbl_contains(meta.groups, dir_meta.group) then
rwx = bit.rshift(meta.mode, 3) rwx = bit.rshift(dir_meta.mode, 3)
else else
rwx = meta.mode rwx = dir_meta.mode
end end
return bit.band(rwx, 2) ~= 0 return bit.band(rwx, 2) ~= 0
end 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 ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
@ -399,16 +267,16 @@ M.perform_action = function(action, cb)
local res = parse_url(action.url) local res = parse_url(action.url)
local conn = get_connection(action.url) local conn = get_connection(action.url)
if action.entry_type == "directory" then 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 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 else
conn:run(string.format("touch '%s'", res.path), cb) conn:touch(res.path, cb)
end end
elseif action.type == "delete" then elseif action.type == "delete" then
local res = parse_url(action.url) local res = parse_url(action.url)
local conn = get_connection(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 elseif action.type == "move" then
local src_adapter = config.get_adapter_by_scheme(action.src_url) local src_adapter = config.get_adapter_by_scheme(action.src_url)
local dest_adapter = config.get_adapter_by_scheme(action.dest_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 src_conn = get_connection(action.src_url)
local dest_conn = get_connection(action.dest_url) local dest_conn = get_connection(action.dest_url)
if src_conn ~= dest_conn then 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 if err then
return cb(err) return cb(err)
end end
src_conn:run(string.format("rm -rf '%s'", src_res.path), cb) src_conn:rm(src_res.path, cb)
end) end)
else 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 end
else else
cb("We should never attempt to move across adapters") 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 src_conn = get_connection(action.src_url)
local dest_conn = get_connection(action.dest_url) local dest_conn = get_connection(action.dest_url)
if src_conn.host ~= dest_conn.host then 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 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 else
local src_arg local src_arg
local dest_arg local dest_arg
@ -454,7 +322,7 @@ M.perform_action = function(action, cb)
src_arg = fs.posix_to_os_path(path) src_arg = fs.posix_to_os_path(path)
dest_arg = url_to_scp(parse_url(action.dest_url)) dest_arg = url_to_scp(parse_url(action.dest_url))
end end
shell.run({ "scp", "-r", src_arg, dest_arg }, cb) shell.run({ "scp", "-C", "-r", src_arg, dest_arg }, cb)
end end
else else
cb(string.format("Bad action type: %s", action.type)) cb(string.format("Bad action type: %s", action.type))
@ -463,4 +331,63 @@ end
M.supports_xfer = { files = true } 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 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 cache = require("oil.cache")
local M = {} local M = {}
---@param url string
---@param callback fun(url: string)
M.normalize_url = function(url, callback)
callback(url)
end
---@param path string ---@param path string
---@param column_defs string[] ---@param column_defs string[]
---@param cb fun(err: nil|string, entries: nil|oil.InternalEntry[]) ---@param cb fun(err: nil|string, entries: nil|oil.InternalEntry[])
@ -36,11 +42,6 @@ M.is_modifiable = function(bufnr)
return true return true
end end
---@param url string
M.url_to_buffer_name = function(url)
error("Test adapter cannot open files")
end
---@param action oil.Action ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
@ -59,4 +60,14 @@ M.perform_action = function(action, cb)
cb() cb()
end end
---@param bufnr integer
M.read_file = function(bufnr)
-- pass
end
---@param bufnr integer
M.write_file = function(bufnr)
-- pass
end
return M 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. -- reason, I'm taking them out of the section above so they won't show up in the autogen docs.
default_config.adapters = { default_config.adapters = {
["oil://"] = "files", ["oil://"] = "files",
["oil-ssh://"] = "ssh", ["scp://"] = "ssh",
} }
-- When opening the parent of a file, substitute these url schemes -- For backwards compatibility
default_config.remap_schemes = { default_config.adapter_aliases = {
["scp://"] = "oil-ssh://", ["oil-ssh://"] = "scp://",
["sftp://"] = "oil-ssh://",
} }
local M = {} local M = {}

View file

@ -14,13 +14,14 @@ local M = {}
---@field name string ---@field name string
---@field list fun(path: string, cb: fun(err: nil|string, entries: nil|oil.InternalEntry[])) ---@field list fun(path: string, cb: fun(err: nil|string, entries: nil|oil.InternalEntry[]))
---@field is_modifiable fun(bufnr: integer): boolean ---@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 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 get_parent nil|fun(bufname: string): string
---@field supports_xfer nil|table<string, boolean> ---@field supports_xfer nil|table<string, boolean>
---@field render_action nil|fun(action: oil.Action): string ---@field render_action nil|fun(action: oil.Action): string
---@field perform_action nil|fun(action: oil.Action, cb: fun(err: nil|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) ---Get the entry on a specific line (1-indexed)
---@param bufnr integer ---@param bufnr integer
@ -189,7 +190,6 @@ M.get_buffer_parent_url = function(bufname)
local parent_url = util.addslash(scheme .. parent) local parent_url = util.addslash(scheme .. parent)
return parent_url, basename return parent_url, basename
else else
scheme = config.remap_schemes[scheme] or scheme
local adapter = config.get_adapter_by_scheme(scheme) local adapter = config.get_adapter_by_scheme(scheme)
local parent_url local parent_url
if adapter and adapter.get_parent then if adapter and adapter.get_parent then
@ -370,7 +370,6 @@ M.select = function(opts)
local scheme, dir = util.parse_url(bufname) local scheme, dir = util.parse_url(bufname)
local child = dir .. entry.name local child = dir .. entry.name
local url = scheme .. child local url = scheme .. child
local buffer_name
if if
entry.type == "directory" entry.type == "directory"
or ( or (
@ -380,7 +379,6 @@ M.select = function(opts)
and entry.meta.link_stat.type == "directory" and entry.meta.link_stat.type == "directory"
) )
then then
buffer_name = util.addslash(url)
-- If this is a new directory BUT we think we already have an entry with this name, disallow -- 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. -- 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. -- 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 if util.is_floating_win() then
vim.api.nvim_win_close(0, false) vim.api.nvim_win_close(0, false)
end end
buffer_name = adapter.url_to_buffer_name(url)
end end
local mods = { local mods = {
vertical = opts.vertical, vertical = opts.vertical,
@ -405,7 +402,7 @@ M.select = function(opts)
local cmd = opts.split and "split" or "edit" local cmd = opts.split and "split" or "edit"
vim.cmd({ vim.cmd({
cmd = cmd, cmd = cmd,
args = { buffer_name }, args = { url },
mods = mods, mods = mods,
}) })
if opts.preview then if opts.preview then
@ -514,19 +511,69 @@ M.save = function(opts)
mutator.try_write_changes(opts.confirm) mutator.try_write_changes(opts.confirm)
end 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 ---@param bufnr integer
local function load_oil_buffer(bufnr) local function load_oil_buffer(bufnr)
local config = require("oil.config") local config = require("oil.config")
local keymap_util = require("oil.keymap_util")
local loading = require("oil.loading") local loading = require("oil.loading")
local util = require("oil.util") local util = require("oil.util")
local view = require("oil.view") local view = require("oil.view")
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = config.get_adapter_by_scheme(bufname) local scheme, path = util.parse_url(bufname)
vim.bo[bufnr].buftype = "acwrite" if config.adapter_aliases[scheme] then
vim.bo[bufnr].filetype = "oil" if scheme == "oil-ssh://" then
vim.bo[bufnr].bufhidden = "hide" vim.notify_once(
vim.bo[bufnr].syntax = "oil" '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) loading.set_loading(bufnr, true)
local function finish(new_url) local function finish(new_url)
if new_url ~= bufname then if new_url ~= bufname then
@ -535,23 +582,31 @@ local function load_oil_buffer(bufnr)
-- have BufReadCmd called for it -- have BufReadCmd called for it
return return
end end
bufname = new_url
end end
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { emsg_silent = true } }) if vim.endswith(bufname, "/") then
view.initialize(bufnr) vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { emsg_silent = true } })
vim.cmd.doautocmd({ args = { "BufReadPost", 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 end
if adapter.normalize_url then adapter.normalize_url(bufname, finish)
adapter.normalize_url(bufname, finish)
else
finish(util.addslash(bufname))
end
end end
---Initialize oil ---Initialize oil
---@param opts nil|table ---@param opts nil|table
M.setup = function(opts) M.setup = function(opts)
local config = require("oil.config") local config = require("oil.config")
-- Disable netrw
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
config.setup(opts) config.setup(opts)
set_colors() set_colors()
vim.api.nvim_create_user_command("Oil", function(args) vim.api.nvim_create_user_command("Oil", function(args)
@ -574,6 +629,9 @@ M.setup = function(opts)
for scheme in pairs(config.adapters) do for scheme in pairs(config.adapters) do
table.insert(patterns, scheme .. "*") table.insert(patterns, scheme .. "*")
end end
for scheme in pairs(config.adapter_aliases) do
table.insert(patterns, scheme .. "*")
end
local scheme_pattern = table.concat(patterns, ",") local scheme_pattern = table.concat(patterns, ",")
vim.api.nvim_create_autocmd("ColorScheme", { vim.api.nvim_create_autocmd("ColorScheme", {
@ -595,10 +653,16 @@ M.setup = function(opts)
pattern = scheme_pattern, pattern = scheme_pattern,
nested = true, nested = true,
callback = function(params) callback = function(params)
vim.cmd.doautocmd({ args = { "BufWritePre", params.file }, mods = { silent = true } }) local bufname = vim.api.nvim_buf_get_name(params.buf)
M.save() if vim.endswith(bufname, "/") then
vim.bo[params.buf].modified = false vim.cmd.doautocmd({ args = { "BufWritePre", params.file }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { "BufWritePost", 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, end,
}) })
vim.api.nvim_create_autocmd("BufWinLeave", { vim.api.nvim_create_autocmd("BufWinLeave", {
@ -617,34 +681,15 @@ M.setup = function(opts)
group = aug, group = aug,
pattern = "*", pattern = "*",
callback = function() callback = function()
local util = require("oil.util")
local view = require("oil.view") local view = require("oil.view")
if vim.bo.filetype == "oil" then local scheme = util.parse_url(vim.api.nvim_buf_get_name(0))
view.set_win_options() if scheme and config.adapters[scheme] then
vim.api.nvim_win_set_var(0, "oil_did_enter", true)
view.maybe_set_cursor() view.maybe_set_cursor()
elseif vim.w.oil_did_enter then else
vim.api.nvim_win_del_var(0, "oil_did_enter") -- Only run this logic if we are *not* in an oil buffer.
-- We are entering a non-oil buffer *after* having been in an oil buffer -- Oil buffers have to run it in BufReadCmd after confirming they are a directory or a file
local has_orig, orig_buffer = pcall(vim.api.nvim_win_get_var, 0, "oil_original_buffer") restore_alt_buf()
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
end, end,
}) })
@ -722,6 +767,7 @@ M.setup = function(opts)
end end
end, end,
}) })
maybe_hijack_directory_buffer(0) maybe_hijack_directory_buffer(0)
end end

View file

@ -487,6 +487,7 @@ M.try_write_changes = function(confirm)
end end
local actions = M.create_actions_from_diffs(all_diffs) 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) disclaimer.show(function(disclaimed)
if not disclaimed then if not disclaimed then
return unlock() return unlock()

View file

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

View file

@ -98,11 +98,10 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
end end
local bufname = vim.api.nvim_buf_get_name(src_bufnr) local bufname = vim.api.nvim_buf_get_name(src_bufnr)
local scheme = M.parse_url(bufname) -- If this buffer is not literally a file on disk, then we can use the simple
-- If this buffer has a scheme (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 -- 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. -- 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 -- This will fail if the dest buf name already exists
local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name) local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name)
if ok then if ok then
@ -159,19 +158,10 @@ end
local function get_possible_buffer_names_from_url(url) local function get_possible_buffer_names_from_url(url)
local fs = require("oil.fs") local fs = require("oil.fs")
local scheme, path = M.parse_url(url) local scheme, path = M.parse_url(url)
local ret = {} if config.adapters[scheme] == "files" then
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
return { fs.posix_to_os_path(path) } return { fs.posix_to_os_path(path) }
else
return ret
end end
return { url }
end end
---@param entry_type oil.EntryType ---@param entry_type oil.EntryType

View file

@ -178,6 +178,12 @@ M.initialize = function(bufnr)
if bufnr == 0 then if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf() bufnr = vim.api.nvim_get_current_buf()
end 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 session[bufnr] = true
for k, v in pairs(config.buf_options) do for k, v in pairs(config.buf_options) do
vim.api.nvim_buf_set_option(bufnr, k, v) vim.api.nvim_buf_set_option(bufnr, k, v)

View file

@ -1,56 +1,67 @@
require("plenary.async").tests.add_to_env()
local oil = require("oil") local oil = require("oil")
local test_util = require("tests.test_util") local test_util = require("tests.test_util")
local fs = require("oil.fs") local fs = require("oil.fs")
describe("Alternate buffer", function() a.describe("Alternate buffer", function()
after_each(function() after_each(function()
test_util.reset_editor() test_util.reset_editor()
end) end)
it("sets previous buffer as alternate", function() a.it("sets previous buffer as alternate", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
vim.cmd.edit({ args = { "bar" } }) vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)
it("sets previous buffer as alternate when editing oil://", function() a.it("sets previous buffer as alternate when editing oil://", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "oil://" .. fs.os_to_posix_path(vim.fn.getcwd()) } }) vim.cmd.edit({ args = { "oil://" .. fs.os_to_posix_path(vim.fn.getcwd()) } })
test_util.wait_for_autocmd("BufReadPost")
vim.cmd.edit({ args = { "bar" } }) vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)
it("preserves alternate buffer if editing the same file", function() a.it("preserves alternate buffer if editing the same file", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "bar" } }) vim.cmd.edit({ args = { "bar" } })
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
vim.cmd.edit({ args = { "bar" } }) vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)
it("preserves alternate buffer if discarding changes", function() a.it("preserves alternate buffer if discarding changes", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "bar" } }) vim.cmd.edit({ args = { "bar" } })
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
oil.close() oil.close()
assert.equals("bar", vim.fn.expand("%"))
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)
it("sets previous buffer as alternate after multi-dir hops", function() a.it("sets previous buffer as alternate after multi-dir hops", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
vim.cmd.edit({ args = { "bar" } }) vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)
describe("floating window", function() a.describe("floating window", function()
it("sets previous buffer as alternate", function() a.it("sets previous buffer as alternate", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
oil.open_float() oil.open_float()
test_util.wait_for_autocmd("BufReadPost")
-- This is lazy, but testing the actual select logic is more difficult. We can simply -- This is lazy, but testing the actual select logic is more difficult. We can simply
-- replicated it by closing the current window and then doing the edit -- replicated it by closing the current window and then doing the edit
vim.api.nvim_win_close(0, true) vim.api.nvim_win_close(0, true)
@ -58,10 +69,11 @@ describe("Alternate buffer", function()
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)
it("preserves alternate buffer if editing the same file", function() a.it("preserves alternate buffer if editing the same file", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "bar" } }) vim.cmd.edit({ args = { "bar" } })
oil.open_float() oil.open_float()
test_util.wait_for_autocmd("BufReadPost")
-- This is lazy, but testing the actual select logic is more difficult. We can simply -- This is lazy, but testing the actual select logic is more difficult. We can simply
-- replicated it by closing the current window and then doing the edit -- replicated it by closing the current window and then doing the edit
vim.api.nvim_win_close(0, true) vim.api.nvim_win_close(0, true)
@ -69,10 +81,11 @@ describe("Alternate buffer", function()
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)
it("preserves alternate buffer if discarding changes", function() a.it("preserves alternate buffer if discarding changes", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "bar" } }) vim.cmd.edit({ args = { "bar" } })
oil.open_float() oil.open_float()
test_util.wait_for_autocmd("BufReadPost")
oil.close() oil.close()
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)

View file

@ -0,0 +1,59 @@
local fs = require("oil.fs")
local test_util = require("tests.test_util")
local util = require("oil.util")
describe("update_moved_buffers", function()
after_each(function()
test_util.reset_editor()
end)
it("Renames moved buffers", function()
vim.cmd.edit({ args = { "oil-test:///foo/bar.txt" } })
util.update_moved_buffers("file", "oil-test:///foo/bar.txt", "oil-test:///foo/baz.txt")
assert.equals("oil-test:///foo/baz.txt", vim.api.nvim_buf_get_name(0))
end)
it("Renames moved buffers when they are normal files", function()
local tmpdir = fs.join(vim.loop.fs_realpath(vim.fn.stdpath("cache")), "oil", "test")
local testfile = fs.join(tmpdir, "foo.txt")
vim.cmd.edit({ args = { testfile } })
util.update_moved_buffers(
"file",
"oil://" .. fs.os_to_posix_path(testfile),
"oil://" .. fs.os_to_posix_path(fs.join(tmpdir, "bar.txt"))
)
assert.equals(fs.join(tmpdir, "bar.txt"), vim.api.nvim_buf_get_name(0))
end)
it("Renames directories", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
util.update_moved_buffers("directory", "oil-test:///foo/", "oil-test:///bar/")
assert.equals("oil-test:///bar/", vim.api.nvim_buf_get_name(0))
end)
it("Renames subdirectories", function()
vim.cmd.edit({ args = { "oil-test:///foo/bar/" } })
util.update_moved_buffers("directory", "oil-test:///foo/", "oil-test:///baz/")
assert.equals("oil-test:///baz/bar/", vim.api.nvim_buf_get_name(0))
end)
it("Renames subfiles", function()
vim.cmd.edit({ args = { "oil-test:///foo/bar.txt" } })
util.update_moved_buffers("directory", "oil-test:///foo/", "oil-test:///baz/")
assert.equals("oil-test:///baz/bar.txt", vim.api.nvim_buf_get_name(0))
end)
it("Renames subfiles when they are normal files", function()
local tmpdir = fs.join(vim.loop.fs_realpath(vim.fn.stdpath("cache")), "oil", "test")
local foo = fs.join(tmpdir, "foo")
local bar = fs.join(tmpdir, "bar")
local testfile = fs.join(foo, "foo.txt")
vim.cmd.edit({ args = { testfile } })
util.update_moved_buffers(
"directory",
"oil://" .. fs.os_to_posix_path(foo),
"oil://" .. fs.os_to_posix_path(bar)
)
assert.equals(fs.join(bar, "foo.txt"), vim.api.nvim_buf_get_name(0))
end)
end)

View file

@ -15,14 +15,14 @@ a.describe("regression tests", function()
vim.cmd.wincmd({ args = { "p" } }) vim.cmd.wincmd({ args = { "p" } })
assert.equals("markdown", vim.bo.filetype) assert.equals("markdown", vim.bo.filetype)
vim.cmd.edit({ args = { "%:p:h" } }) vim.cmd.edit({ args = { "%:p:h" } })
a.util.sleep(10) test_util.wait_for_autocmd("BufReadPost")
assert.equals("oil", vim.bo.filetype) assert.equals("oil", vim.bo.filetype)
end) end)
-- https://github.com/stevearc/oil.nvim/issues/37 -- https://github.com/stevearc/oil.nvim/issues/37
a.it("places the cursor on correct entry when opening on file", function() a.it("places the cursor on correct entry when opening on file", function()
vim.cmd.edit({ args = { "." } }) vim.cmd.edit({ args = { "." } })
a.util.sleep(10) test_util.wait_for_autocmd("BufReadPost")
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
assert.not_equals("README.md", entry and entry.name) assert.not_equals("README.md", entry and entry.name)
vim.cmd.edit({ args = { "README.md" } }) vim.cmd.edit({ args = { "README.md" } })

View file

@ -1,3 +1,4 @@
require("plenary.async").tests.add_to_env()
local M = {} local M = {}
M.reset_editor = function() M.reset_editor = function()
@ -16,4 +17,13 @@ M.reset_editor = function()
end end
end end
M.wait_for_autocmd = a.wrap(function(autocmd, cb)
vim.api.nvim_create_autocmd(autocmd, {
pattern = "*",
nested = true,
once = true,
callback = vim.schedule_wrap(cb),
})
end, 2)
return M return M

View file

@ -7,8 +7,8 @@ describe("url", function()
{ "/foo/bar.txt", "oil:///foo/", "bar.txt" }, { "/foo/bar.txt", "oil:///foo/", "bar.txt" },
{ "oil:///foo/bar.txt", "oil:///foo/", "bar.txt" }, { "oil:///foo/bar.txt", "oil:///foo/", "bar.txt" },
{ "oil:///", "oil:///" }, { "oil:///", "oil:///" },
{ "scp://user@hostname:8888//bar.txt", "oil-ssh://user@hostname:8888//", "bar.txt" }, { "scp://user@hostname:8888//bar.txt", "scp://user@hostname:8888//", "bar.txt" },
{ "oil-ssh://user@hostname:8888//", "oil-ssh://user@hostname:8888//" }, { "scp://user@hostname:8888//", "scp://user@hostname:8888//" },
} }
for _, case in ipairs(cases) do for _, case in ipairs(cases) do
local input, expected, expected_basename = unpack(case) local input, expected, expected_basename = unpack(case)

View file

@ -1,35 +1,40 @@
require("plenary.async").tests.add_to_env()
local oil = require("oil") local oil = require("oil")
local test_util = require("tests.test_util") local test_util = require("tests.test_util")
describe("window options", function() a.describe("window options", function()
after_each(function() after_each(function()
test_util.reset_editor() test_util.reset_editor()
end) end)
it("Restores window options on close", function() a.it("Restores window options on close", function()
vim.cmd.edit({ args = { "README.md" } }) vim.cmd.edit({ args = { "README.md" } })
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
oil.close() oil.close()
assert.equals("auto", vim.o.signcolumn) assert.equals("auto", vim.o.signcolumn)
end) end)
it("Restores window options on edit", function() a.it("Restores window options on edit", function()
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
vim.cmd.edit({ args = { "README.md" } }) vim.cmd.edit({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn) assert.equals("auto", vim.o.signcolumn)
end) end)
it("Restores window options on split <filename>", function() a.it("Restores window options on split <filename>", function()
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
vim.cmd.split({ args = { "README.md" } }) vim.cmd.split({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn) assert.equals("auto", vim.o.signcolumn)
end) end)
it("Restores window options on split", function() a.it("Restores window options on split", function()
oil.open() oil.open()
test_util.wait_for_autocmd("BufReadPost")
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
vim.cmd.split() vim.cmd.split()
vim.cmd.edit({ args = { "README.md" } }) vim.cmd.edit({ args = { "README.md" } })