diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 1da95f4..e5468c8 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -138,11 +138,32 @@ M.toggle_hidden = { M.open_terminal = { desc = "Open a terminal in the current directory", callback = function() - local dir = oil.get_current_dir() - if dir then + local config = require("oil.config") + local bufname = vim.api.nvim_buf_get_name(0) + local adapter = config.get_adapter_by_scheme(bufname) + if not adapter then + return + end + if adapter.name == "files" then + local dir = oil.get_current_dir() + assert(dir, "Oil buffer with files adapter must have current directory") local bufnr = vim.api.nvim_create_buf(false, true) vim.api.nvim_set_current_buf(bufnr) vim.fn.termopen(vim.o.shell, { cwd = dir }) + elseif adapter.name == "ssh" then + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_current_buf(bufnr) + local url = require("oil.adapters.ssh").parse_url(bufname) + local cmd = require("oil.adapters.ssh.connection").create_ssh_command(url) + local term_id = vim.fn.termopen(cmd) + if term_id then + vim.api.nvim_chan_send(term_id, string.format("cd %s\n", url.path)) + end + else + vim.notify( + string.format("Cannot open terminal for unsupported adapter: '%s'", adapter.name), + vim.log.levels.WARN + ) end end, } diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua index d8cc33b..cc21051 100644 --- a/lua/oil/adapters/ssh.lua +++ b/lua/oil/adapters/ssh.lua @@ -22,8 +22,9 @@ local FIELD_META = constants.FIELD_META ---@param oil_url string ---@return oil.sshUrl -local function parse_url(oil_url) +M.parse_url = function(oil_url) local scheme, url = util.parse_url(oil_url) + assert(scheme and url, string.format("Malformed input url '%s'", oil_url)) local ret = { scheme = scheme } local username, rem = url:match("^([^@%s]+)@(.*)$") ret.user = username @@ -80,11 +81,19 @@ local function url_to_scp(url) return table.concat(pieces, "") end +---@param url1 oil.sshUrl +---@param url2 oil.sshUrl +---@return boolean +local function url_hosts_equal(url1, url2) + return url1.host == url2.host and url1.port == url2.port and url1.user == url2.user +end + local _connections = {} ---@param url string ---@param allow_retry nil|boolean +---@return oil.sshFs local function get_connection(url, allow_retry) - local res = parse_url(url) + local res = M.parse_url(url) res.scheme = config.adapter_to_scheme.ssh res.path = "" local key = url_to_str(res) @@ -124,7 +133,7 @@ ssh_columns.permissions = { end, perform_action = function(action, callback) - local res = parse_url(action.url) + local res = M.parse_url(action.url) local conn = get_connection(action.url) conn:chmod(action.value, res.path, callback) end, @@ -168,7 +177,7 @@ end ---@param bufname string ---@return string M.get_parent = function(bufname) - local res = parse_url(bufname) + local res = M.parse_url(bufname) res.path = pathutil.parent(res.path) return url_to_str(res) end @@ -176,7 +185,7 @@ end ---@param url string ---@param callback fun(url: string) M.normalize_url = function(url, callback) - local res = parse_url(url) + local res = M.parse_url(url) local conn = get_connection(url, true) local path = res.path @@ -199,7 +208,7 @@ end ---@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 res = M.parse_url(url) cache.begin_update_url(url) local conn = get_connection(url) @@ -267,7 +276,7 @@ end ---@param cb fun(err: nil|string) M.perform_action = function(action, cb) if action.type == "create" then - local res = parse_url(action.url) + local res = M.parse_url(action.url) local conn = get_connection(action.url) if action.entry_type == "directory" then conn:mkdir(res.path, cb) @@ -277,15 +286,15 @@ M.perform_action = function(action, cb) conn:touch(res.path, cb) end elseif action.type == "delete" then - local res = parse_url(action.url) + local res = M.parse_url(action.url) local conn = get_connection(action.url) 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) if src_adapter == M and dest_adapter == M then - local src_res = parse_url(action.src_url) - local dest_res = parse_url(action.dest_url) + local src_res = M.parse_url(action.src_url) + local dest_res = M.parse_url(action.dest_url) local src_conn = get_connection(action.src_url) local dest_conn = get_connection(action.dest_url) if src_conn ~= dest_conn then @@ -305,25 +314,25 @@ M.perform_action = function(action, cb) local src_adapter = config.get_adapter_by_scheme(action.src_url) local dest_adapter = config.get_adapter_by_scheme(action.dest_url) if src_adapter == M and dest_adapter == M then - local src_res = parse_url(action.src_url) - local dest_res = parse_url(action.dest_url) - local src_conn = get_connection(action.src_url) - local dest_conn = get_connection(action.dest_url) - if src_conn.host ~= dest_conn.host then + local src_res = M.parse_url(action.src_url) + local dest_res = M.parse_url(action.dest_url) + if not url_hosts_equal(src_res, dest_res) then shell.run({ "scp", "-C", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb) + else + local src_conn = get_connection(action.src_url) + src_conn:cp(src_res.path, dest_res.path, cb) end - src_conn:cp(src_res.path, dest_res.path, cb) else local src_arg local dest_arg if src_adapter == M then - src_arg = url_to_scp(parse_url(action.src_url)) + src_arg = url_to_scp(M.parse_url(action.src_url)) local _, path = util.parse_url(action.dest_url) dest_arg = fs.posix_to_os_path(path) else local _, path = util.parse_url(action.src_url) src_arg = fs.posix_to_os_path(path) - dest_arg = url_to_scp(parse_url(action.dest_url)) + dest_arg = url_to_scp(M.parse_url(action.dest_url)) end shell.run({ "scp", "-C", "-r", src_arg, dest_arg }, cb) end @@ -338,7 +347,7 @@ M.supports_xfer = { files = true } 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 url = M.parse_url(bufname) local scp_url = url_to_scp(url) local basename = pathutil.basename(bufname) local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil") @@ -372,7 +381,7 @@ end 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 url = M.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")) diff --git a/lua/oil/adapters/ssh/connection.lua b/lua/oil/adapters/ssh/connection.lua index a106ecf..8e592d0 100644 --- a/lua/oil/adapters/ssh/connection.lua +++ b/lua/oil/adapters/ssh/connection.lua @@ -1,5 +1,8 @@ local layout = require("oil.layout") local util = require("oil.util") + +---@class oil.sshConnection +---@field meta {user?: string, groups?: string[]} local SSHConnection = {} local function output_extend(agg, output) @@ -44,7 +47,8 @@ local function get_last_lines(bufnr, num_lines) end ---@param url oil.sshUrl -function SSHConnection.new(url) +---@return string[] +function SSHConnection.create_ssh_command(url) local host = url.host if url.user then host = url.user .. "@" .. host @@ -52,19 +56,27 @@ function SSHConnection.new(url) local command = { "ssh", host, + } + if url.port then + table.insert(command, "-p") + table.insert(command, url.port) + end + return command +end + +---@param url oil.sshUrl +---@return oil.sshConnection +function SSHConnection.new(url) + local command = SSHConnection.create_ssh_command(url) + vim.list_extend(command, { "/bin/bash", "--norc", "-c", -- HACK: For some reason in my testing if I just have "echo READY" it doesn't appear, but if I echo -- anything prior to that, it *will* appear. The first line gets swallowed. "echo '_make_newline_'; echo '===READY==='; exec /bin/bash --norc", - } - if url.port then - table.insert(command, 2, "-p") - table.insert(command, 3, url.port) - end + }) local self = setmetatable({ - host = host, meta = {}, commands = {}, connected = false, @@ -84,7 +96,7 @@ function SSHConnection.new(url) }) end) self.term_id = term_id - vim.api.nvim_chan_send(term_id, string.format("ssh %s\r\n", host)) + vim.api.nvim_chan_send(term_id, string.format("ssh %s\r\n", url.host)) util.hack_around_termopen_autocmd(mode) -- If it takes more than 2 seconds to connect, pop open the terminal @@ -131,6 +143,7 @@ function SSHConnection.new(url) if err then vim.notify(string.format("Error fetching ssh connection user: %s", err), vim.log.levels.WARN) else + assert(lines) self.meta.user = vim.trim(table.concat(lines, "")) end end) @@ -141,6 +154,7 @@ function SSHConnection.new(url) vim.log.levels.WARN ) else + assert(lines) self.meta.groups = vim.split(table.concat(lines, ""), "%s+", { trimempty = true }) end end) diff --git a/lua/oil/adapters/ssh/sshfs.lua b/lua/oil/adapters/ssh/sshfs.lua index 47eea01..42d7af7 100644 --- a/lua/oil/adapters/ssh/sshfs.lua +++ b/lua/oil/adapters/ssh/sshfs.lua @@ -3,6 +3,9 @@ local constants = require("oil.constants") local permissions = require("oil.adapters.files.permissions") local SSHConnection = require("oil.adapters.ssh.connection") local util = require("oil.util") + +---@class oil.sshFs +---@field conn oil.sshConnection local SSHFS = {} local FIELD_TYPE = constants.FIELD_TYPE @@ -20,7 +23,7 @@ local typechar_map = { ---@param line string ---@return string Name of entry ---@return oil.EntryType ----@return nil|table Metadata for entry +---@return 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+(.*)$") @@ -58,6 +61,7 @@ local function parse_ls_line(line) end ---@param url oil.sshUrl +---@return oil.sshFs function SSHFS.new(url) return setmetatable({ conn = SSHConnection.new(url), @@ -94,6 +98,7 @@ function SSHFS:realpath(path, callback) if err then return callback(err) end + assert(lines) 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 @@ -105,6 +110,7 @@ function SSHFS:realpath(path, callback) -- If the file doesn't exist, treat it like a not-yet-existing directory type = "directory" else + assert(ls_lines) local _ _, type = parse_ls_line(ls_lines[1]) end @@ -133,6 +139,7 @@ function SSHFS:list_dir(url, path, callback) return callback(err) end end + assert(lines) local any_links = false local entries = {} for _, line in ipairs(lines) do @@ -159,6 +166,7 @@ function SSHFS:list_dir(url, path, callback) if link_err and not link_err:match("^1:") then return callback(link_err) end + assert(link_lines) 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)