feat: builtin support for editing files over ssh (#27)
This commit is contained in:
parent
75b710e311
commit
ca4da68aae
18 changed files with 593 additions and 291 deletions
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
|
|
@ -37,6 +37,7 @@ jobs:
|
|||
include:
|
||||
- nvim_tag: v0.8.0
|
||||
- nvim_tag: v0.8.1
|
||||
- nvim_tag: v0.8.2
|
||||
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
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.
|
||||
This may look familiar. In fact, this is the exact same url format that netrw uses.
|
||||
|
||||
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`).
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
228
lua/oil/adapters/ssh/sshfs.lua
Normal file
228
lua/oil/adapters/ssh/sshfs.lua
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
146
lua/oil/init.lua
146
lua/oil/init.lua
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,56 +1,67 @@
|
|||
require("plenary.async").tests.add_to_env()
|
||||
local oil = require("oil")
|
||||
local test_util = require("tests.test_util")
|
||||
local fs = require("oil.fs")
|
||||
|
||||
describe("Alternate buffer", function()
|
||||
a.describe("Alternate buffer", function()
|
||||
after_each(function()
|
||||
test_util.reset_editor()
|
||||
end)
|
||||
|
||||
it("sets previous buffer as alternate", function()
|
||||
a.it("sets previous buffer as alternate", function()
|
||||
vim.cmd.edit({ args = { "foo" } })
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
vim.cmd.edit({ args = { "bar" } })
|
||||
assert.equals("foo", vim.fn.expand("#"))
|
||||
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 = { "oil://" .. fs.os_to_posix_path(vim.fn.getcwd()) } })
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
vim.cmd.edit({ args = { "bar" } })
|
||||
assert.equals("foo", vim.fn.expand("#"))
|
||||
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 = { "bar" } })
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
vim.cmd.edit({ args = { "bar" } })
|
||||
assert.equals("foo", vim.fn.expand("#"))
|
||||
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 = { "bar" } })
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
oil.close()
|
||||
assert.equals("bar", vim.fn.expand("%"))
|
||||
assert.equals("foo", vim.fn.expand("#"))
|
||||
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" } })
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
vim.cmd.edit({ args = { "bar" } })
|
||||
assert.equals("foo", vim.fn.expand("#"))
|
||||
end)
|
||||
|
||||
describe("floating window", function()
|
||||
it("sets previous buffer as alternate", function()
|
||||
a.describe("floating window", function()
|
||||
a.it("sets previous buffer as alternate", function()
|
||||
vim.cmd.edit({ args = { "foo" } })
|
||||
oil.open_float()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
-- 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
|
||||
vim.api.nvim_win_close(0, true)
|
||||
|
|
@ -58,10 +69,11 @@ describe("Alternate buffer", function()
|
|||
assert.equals("foo", vim.fn.expand("#"))
|
||||
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 = { "bar" } })
|
||||
oil.open_float()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
-- 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
|
||||
vim.api.nvim_win_close(0, true)
|
||||
|
|
@ -69,10 +81,11 @@ describe("Alternate buffer", function()
|
|||
assert.equals("foo", vim.fn.expand("#"))
|
||||
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 = { "bar" } })
|
||||
oil.open_float()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
oil.close()
|
||||
assert.equals("foo", vim.fn.expand("#"))
|
||||
end)
|
||||
|
|
|
|||
59
tests/move_rename_spec.lua
Normal file
59
tests/move_rename_spec.lua
Normal 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)
|
||||
|
|
@ -15,14 +15,14 @@ a.describe("regression tests", function()
|
|||
vim.cmd.wincmd({ args = { "p" } })
|
||||
assert.equals("markdown", vim.bo.filetype)
|
||||
vim.cmd.edit({ args = { "%:p:h" } })
|
||||
a.util.sleep(10)
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
assert.equals("oil", vim.bo.filetype)
|
||||
end)
|
||||
|
||||
-- https://github.com/stevearc/oil.nvim/issues/37
|
||||
a.it("places the cursor on correct entry when opening on file", function()
|
||||
vim.cmd.edit({ args = { "." } })
|
||||
a.util.sleep(10)
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
local entry = oil.get_cursor_entry()
|
||||
assert.not_equals("README.md", entry and entry.name)
|
||||
vim.cmd.edit({ args = { "README.md" } })
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
require("plenary.async").tests.add_to_env()
|
||||
local M = {}
|
||||
|
||||
M.reset_editor = function()
|
||||
|
|
@ -16,4 +17,13 @@ M.reset_editor = function()
|
|||
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
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ describe("url", function()
|
|||
{ "/foo/bar.txt", "oil:///foo/", "bar.txt" },
|
||||
{ "oil:///foo/bar.txt", "oil:///foo/", "bar.txt" },
|
||||
{ "oil:///", "oil:///" },
|
||||
{ "scp://user@hostname:8888//bar.txt", "oil-ssh://user@hostname:8888//", "bar.txt" },
|
||||
{ "oil-ssh://user@hostname:8888//", "oil-ssh://user@hostname:8888//" },
|
||||
{ "scp://user@hostname:8888//bar.txt", "scp://user@hostname:8888//", "bar.txt" },
|
||||
{ "scp://user@hostname:8888//", "scp://user@hostname:8888//" },
|
||||
}
|
||||
for _, case in ipairs(cases) do
|
||||
local input, expected, expected_basename = unpack(case)
|
||||
|
|
|
|||
|
|
@ -1,35 +1,40 @@
|
|||
require("plenary.async").tests.add_to_env()
|
||||
local oil = require("oil")
|
||||
local test_util = require("tests.test_util")
|
||||
|
||||
describe("window options", function()
|
||||
a.describe("window options", function()
|
||||
after_each(function()
|
||||
test_util.reset_editor()
|
||||
end)
|
||||
|
||||
it("Restores window options on close", function()
|
||||
a.it("Restores window options on close", function()
|
||||
vim.cmd.edit({ args = { "README.md" } })
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
assert.equals("no", vim.o.signcolumn)
|
||||
oil.close()
|
||||
assert.equals("auto", vim.o.signcolumn)
|
||||
end)
|
||||
|
||||
it("Restores window options on edit", function()
|
||||
a.it("Restores window options on edit", function()
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
assert.equals("no", vim.o.signcolumn)
|
||||
vim.cmd.edit({ args = { "README.md" } })
|
||||
assert.equals("auto", vim.o.signcolumn)
|
||||
end)
|
||||
|
||||
it("Restores window options on split <filename>", function()
|
||||
a.it("Restores window options on split <filename>", function()
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
assert.equals("no", vim.o.signcolumn)
|
||||
vim.cmd.split({ args = { "README.md" } })
|
||||
assert.equals("auto", vim.o.signcolumn)
|
||||
end)
|
||||
|
||||
it("Restores window options on split", function()
|
||||
a.it("Restores window options on split", function()
|
||||
oil.open()
|
||||
test_util.wait_for_autocmd("BufReadPost")
|
||||
assert.equals("no", vim.o.signcolumn)
|
||||
vim.cmd.split()
|
||||
vim.cmd.edit({ args = { "README.md" } })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue