diff --git a/doc/oil.txt b/doc/oil.txt
index 71d8d70..b2ccef7 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -173,6 +173,8 @@ The full list of options with their defaults:
extra_scp_args = {},
-- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
extra_s3_args = {},
+ -- Extra arguments to pass to curl for FTP operations
+ extra_curl_args = {},
-- EXPERIMENTAL support for performing file operations with git
git = {
-- Return true to automatically git add/mv/rm files
@@ -993,6 +995,50 @@ S3 *oil-adapter-s
Older versions of Neovim (0.11 and earlier) don't support numbers in the
URL scheme, so use `oil-sss` instead of `oil-s3`.
+FTP *oil-adapter-ftp*
+
+ Browse files over FTP or FTPS (FTP over TLS). Open a buffer with: >
+ nvim oil-ftp://[username[:password]@]hostname[:port]/[path]/
+ nvim oil-ftps://[username[:password]@]hostname[:port]/[path]/
+<
+ The `oil-ftps://` scheme connects with `--ssl-reqd`, requiring a valid TLS
+ certificate. Use `oil-ftp://` for plain FTP.
+
+ Authentication ~
+
+ Credentials can be supplied in the URL (`user:pass@host`) or stored in
+ `~/.netrc` (recommended — keeps passwords out of shell history): >
+ machine ftp.example.com login myuser password mypass
+<
+ How it works ~
+
+ The FTP adapter uses `curl` to perform all operations. Directory listings
+ come from FTP LIST output (Unix and IIS/Windows formats are both supported).
+ File reads download to a local tempfile; file writes upload from a tempfile.
+ Renames on the same server use the FTP RNFR/RNTO commands. File copies
+ between servers go through a local tempfile.
+
+ Limitations ~
+
+ Symbolic links cannot be created over FTP. Directory copies are not
+ supported (copy individual files instead). Moving or copying directories
+ across different FTP hosts is not supported.
+
+ Configuration ~
+
+ Pass extra flags to `curl` with `extra_curl_args`: >lua
+ require("oil").setup({
+ extra_curl_args = { "--insecure" },
+ })
+<
+ The adapter supports the `size`, `mtime`, and `permissions` columns.
+ Permission changes use the FTP `SITE CHMOD` command; not all servers
+ support it.
+
+ Dependencies ~
+
+ Requires `curl` (standard on Linux and macOS).
+
Trash *oil-adapter-trash*
See |oil-trash| for details on the built-in trash adapter.
diff --git a/doc/upstream.md b/doc/upstream.md
index bffee00..925db5f 100644
--- a/doc/upstream.md
+++ b/doc/upstream.md
@@ -40,7 +40,7 @@ issues against this fork.
| [#156](https://github.com/stevearc/oil.nvim/issues/156) | Paste path of files into oil buffer | fixed — added `oil-recipe-paste-file-from-clipboard` |
| [#200](https://github.com/stevearc/oil.nvim/issues/200) | Highlights not working when opening a file | not actionable — cannot reproduce, nvim 0.9.4 |
| [#207](https://github.com/stevearc/oil.nvim/issues/207) | Suppress "no longer available" message | fixed — `cleanup_buffers_on_delete` option |
-| [#210](https://github.com/stevearc/oil.nvim/issues/210) | FTP support | open |
+| [#210](https://github.com/stevearc/oil.nvim/issues/210) | FTP support | fixed |
| [#213](https://github.com/stevearc/oil.nvim/issues/213) | Disable preview for large files | fixed ([#85](https://github.com/barrettruth/canola.nvim/pull/85)) |
| [#226](https://github.com/stevearc/oil.nvim/issues/226) | K8s/Docker adapter | not actionable — no demand |
| [#232](https://github.com/stevearc/oil.nvim/issues/232) | Cannot close last window | consolidated into [#149](https://github.com/barrettruth/canola.nvim/issues/149) |
diff --git a/lua/oil/adapters/ftp.lua b/lua/oil/adapters/ftp.lua
new file mode 100644
index 0000000..3d8580c
--- /dev/null
+++ b/lua/oil/adapters/ftp.lua
@@ -0,0 +1,639 @@
+local config = require('oil.config')
+local constants = require('oil.constants')
+local files = require('oil.adapters.files')
+local fs = require('oil.fs')
+local loading = require('oil.loading')
+local pathutil = require('oil.pathutil')
+local permissions = require('oil.adapters.files.permissions')
+local shell = require('oil.shell')
+local util = require('oil.util')
+local M = {}
+
+local FIELD_TYPE = constants.FIELD_TYPE
+local FIELD_META = constants.FIELD_META
+
+---@class (exact) oil.ftpUrl
+---@field scheme string
+---@field host string
+---@field user nil|string
+---@field password nil|string
+---@field port nil|integer
+---@field path string
+
+---@param oil_url string
+---@return oil.ftpUrl
+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 userinfo, rem = url:match('^([^@%s]+)@(.*)$')
+ if userinfo then
+ local user, pass = userinfo:match('^([^:]+):(.+)$')
+ if user then
+ ret.user = user
+ ret.password = pass
+ else
+ ret.user = userinfo
+ end
+ url = rem
+ end
+ local host, port, path = url:match('^([^:]+):(%d+)/(.*)$')
+ if host then
+ ret.host = host
+ ret.port = tonumber(port)
+ ret.path = path
+ else
+ host, path = url:match('^([^/]+)/(.*)$')
+ ret.host = host
+ ret.path = path
+ end
+ if not ret.host or not ret.path then
+ error(string.format('Malformed FTP url: %s', oil_url))
+ end
+ ---@cast ret oil.ftpUrl
+ return ret
+end
+
+---@param url oil.ftpUrl
+---@return string
+local function url_to_str(url)
+ local pieces = { url.scheme }
+ if url.user then
+ table.insert(pieces, url.user)
+ if url.password then
+ table.insert(pieces, ':')
+ table.insert(pieces, url.password)
+ end
+ table.insert(pieces, '@')
+ end
+ table.insert(pieces, url.host)
+ if url.port then
+ table.insert(pieces, string.format(':%d', url.port))
+ end
+ table.insert(pieces, '/')
+ table.insert(pieces, url.path)
+ return table.concat(pieces, '')
+end
+
+---@param url oil.ftpUrl
+---@return string
+local function curl_ftp_url(url)
+ local scheme = url.scheme == 'oil-ftps://' and 'ftps://' or 'ftp://'
+ local pieces = { scheme }
+ if url.user then
+ table.insert(pieces, url.user)
+ if url.password then
+ table.insert(pieces, ':')
+ table.insert(pieces, url.password)
+ end
+ table.insert(pieces, '@')
+ end
+ table.insert(pieces, url.host)
+ if url.port then
+ table.insert(pieces, string.format(':%d', url.port))
+ end
+ table.insert(pieces, '/')
+ table.insert(pieces, url.path)
+ return table.concat(pieces, '')
+end
+
+---@param url oil.ftpUrl
+---@return string
+local function host_ftp_url(url)
+ local scheme = url.scheme == 'oil-ftps://' and 'ftps://' or 'ftp://'
+ local pieces = { scheme }
+ if url.user then
+ table.insert(pieces, url.user)
+ if url.password then
+ table.insert(pieces, ':')
+ table.insert(pieces, url.password)
+ end
+ table.insert(pieces, '@')
+ end
+ table.insert(pieces, url.host)
+ if url.port then
+ table.insert(pieces, string.format(':%d', url.port))
+ end
+ table.insert(pieces, '/')
+ return table.concat(pieces, '')
+end
+
+---@param url oil.ftpUrl
+---@return string[]
+local function ssl_args(url)
+ if url.scheme == 'oil-ftps://' then
+ return { '--ssl-reqd' }
+ end
+ return {}
+end
+
+---@param url oil.ftpUrl
+---@param extra_args string[]
+---@param cb fun(err: nil|string, output: nil|string[])
+local function curl(url, extra_args, cb)
+ local cmd = { 'curl', '-s', '--netrc-optional' }
+ vim.list_extend(cmd, ssl_args(url))
+ vim.list_extend(cmd, config.extra_curl_args)
+ vim.list_extend(cmd, extra_args)
+ shell.run(cmd, cb)
+end
+
+---@param url oil.ftpUrl
+---@return string
+local function ftp_abs_path(url)
+ return '/' .. url.path
+end
+
+---@param url1 oil.ftpUrl
+---@param url2 oil.ftpUrl
+---@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 month_map = {
+ Jan = 1,
+ Feb = 2,
+ Mar = 3,
+ Apr = 4,
+ May = 5,
+ Jun = 6,
+ Jul = 7,
+ Aug = 8,
+ Sep = 9,
+ Oct = 10,
+ Nov = 11,
+ Dec = 12,
+}
+
+---@param line string
+---@return nil|string, nil|string, nil|table
+local function parse_unix_list_line(line)
+ local perms, size, month, day, timeoryear, name =
+ line:match('^([dlrwxstST%-]+)%s+%d+%s+%S+%s+%S+%s+(%d+)%s+(%a+)%s+(%d+)%s+(%S+)%s+(.+)$')
+ if not perms then
+ return nil
+ end
+ local entry_type
+ local first = perms:sub(1, 1)
+ if first == 'd' then
+ entry_type = 'directory'
+ elseif first == 'l' then
+ entry_type = 'link'
+ else
+ entry_type = 'file'
+ end
+ local link_target
+ if entry_type == 'link' then
+ local link_name, target = name:match('^(.+) %-> (.+)$')
+ if link_name then
+ name = link_name
+ link_target = target
+ end
+ end
+ local mtime
+ local mon = month_map[month]
+ if mon then
+ local hour, min = timeoryear:match('^(%d+):(%d+)$')
+ if hour then
+ local now = os.time()
+ local t = os.date('*t', now)
+ ---@cast t osdate
+ mtime = os.time({
+ year = t.year,
+ month = mon,
+ day = tonumber(day),
+ hour = tonumber(hour),
+ min = tonumber(min),
+ sec = 0,
+ })
+ if mtime > now + 86400 then
+ mtime = os.time({
+ year = t.year - 1,
+ month = mon,
+ day = tonumber(day),
+ hour = tonumber(hour),
+ min = tonumber(min),
+ sec = 0,
+ })
+ end
+ else
+ local year = tonumber(timeoryear)
+ if year then
+ mtime =
+ os.time({ year = year, month = mon, day = tonumber(day), hour = 0, min = 0, sec = 0 })
+ end
+ end
+ end
+ local mode = permissions.parse(perms:sub(2))
+ local meta = { size = tonumber(size), mtime = mtime, mode = mode }
+ if link_target then
+ meta.link = link_target
+ end
+ return name, entry_type, meta
+end
+
+---@param line string
+---@return nil|string, nil|string, nil|table
+local function parse_iis_list_line(line)
+ local size_or_dir, name = line:match('^%d+%-%d+%-%d+%s+%d+:%d+%a+%s+(%S+)%s+(.+)$')
+ if not size_or_dir then
+ return nil
+ end
+ local entry_type, size
+ if size_or_dir == '
' then
+ entry_type = 'directory'
+ else
+ entry_type = 'file'
+ size = tonumber(size_or_dir)
+ end
+ local meta = { size = size }
+ return name, entry_type, meta
+end
+
+local ftp_columns = {}
+ftp_columns.size = {
+ render = function(entry, conf)
+ local meta = entry[FIELD_META]
+ if not meta or not meta.size then
+ return ''
+ end
+ if entry[FIELD_TYPE] == 'directory' then
+ return ''
+ end
+ if meta.size >= 1e9 then
+ return string.format('%.1fG', meta.size / 1e9)
+ elseif meta.size >= 1e6 then
+ return string.format('%.1fM', meta.size / 1e6)
+ elseif meta.size >= 1e3 then
+ return string.format('%.1fk', meta.size / 1e3)
+ else
+ return string.format('%d', meta.size)
+ end
+ end,
+
+ parse = function(line, conf)
+ return line:match('^(%d+%S*)%s+(.*)$')
+ end,
+
+ get_sort_value = function(entry)
+ local meta = entry[FIELD_META]
+ if meta and meta.size then
+ return meta.size
+ else
+ return 0
+ end
+ end,
+}
+
+ftp_columns.mtime = {
+ render = function(entry, conf)
+ local meta = entry[FIELD_META]
+ if not meta or not meta.mtime then
+ return ''
+ end
+ return os.date('%Y-%m-%d %H:%M', meta.mtime)
+ end,
+
+ parse = function(line, conf)
+ return line:match('^(%d+%-%d+%-%d+%s%d+:%d+)%s+(.*)$')
+ end,
+
+ get_sort_value = function(entry)
+ local meta = entry[FIELD_META]
+ if meta and meta.mtime then
+ return meta.mtime
+ else
+ return 0
+ end
+ end,
+}
+
+ftp_columns.permissions = {
+ render = function(entry, conf)
+ local meta = entry[FIELD_META]
+ if not meta or not meta.mode then
+ return
+ end
+ local str = permissions.mode_to_str(meta.mode)
+ return { str, permissions.mode_to_highlights(str) }
+ end,
+
+ parse = function(line, conf)
+ return permissions.parse(line)
+ end,
+
+ compare = function(entry, parsed_value)
+ local meta = entry[FIELD_META]
+ if parsed_value and meta and meta.mode then
+ local mask = bit.lshift(1, 12) - 1
+ local old_mode = bit.band(meta.mode, mask)
+ if parsed_value ~= old_mode then
+ return true
+ end
+ end
+ return false
+ end,
+
+ render_action = function(action)
+ return string.format('CHMOD %s %s', permissions.mode_to_octal_str(action.value), action.url)
+ end,
+
+ perform_action = function(action, callback)
+ local res = M.parse_url(action.url)
+ local octal = permissions.mode_to_octal_str(action.value)
+ local ftp_path = ftp_abs_path(res)
+ curl(res, {
+ '--quote',
+ string.format('SITE CHMOD %s %s', octal, ftp_path),
+ host_ftp_url(res),
+ }, callback)
+ end,
+}
+
+---@param name string
+---@return nil|oil.ColumnDefinition
+M.get_column = function(name)
+ return ftp_columns[name]
+end
+
+---@param bufname string
+---@return string
+M.get_parent = function(bufname)
+ local res = M.parse_url(bufname)
+ res.path = pathutil.parent(res.path)
+ return url_to_str(res)
+end
+
+---@param url string
+---@param callback fun(url: string)
+M.normalize_url = function(url, callback)
+ local res = M.parse_url(url)
+ callback(url_to_str(res))
+end
+
+---@param url string
+---@param column_defs string[]
+---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
+M.list = function(url, column_defs, callback)
+ if vim.fn.executable('curl') ~= 1 then
+ callback('`curl` is not executable. Can you run `curl --version`?')
+ return
+ end
+ local res = M.parse_url(url)
+ curl(res, { curl_ftp_url(res) }, function(err, output)
+ if err then
+ callback(err)
+ return
+ end
+ local entries = {}
+ for _, line in ipairs(output or {}) do
+ if line ~= '' then
+ local name, entry_type, meta = parse_unix_list_line(line)
+ if not name then
+ name, entry_type, meta = parse_iis_list_line(line)
+ end
+ if name and entry_type and name ~= '.' and name ~= '..' then
+ table.insert(entries, { 0, name, entry_type, meta })
+ end
+ end
+ end
+ callback(nil, entries)
+ end)
+end
+
+---@param bufnr integer
+---@return boolean
+M.is_modifiable = function(bufnr)
+ return true
+end
+
+---@param action oil.Action
+---@return string
+M.render_action = function(action)
+ if action.type == 'create' then
+ local ret = string.format('CREATE %s', action.url)
+ if action.link then
+ ret = ret .. ' -> ' .. action.link
+ end
+ return ret
+ elseif action.type == 'delete' then
+ return string.format('DELETE %s', action.url)
+ elseif action.type == 'move' or action.type == 'copy' then
+ local src = action.src_url
+ local dest = action.dest_url
+ if config.get_adapter_by_scheme(src) ~= M then
+ local _, path = util.parse_url(src)
+ assert(path)
+ src = files.to_short_os_path(path, action.entry_type)
+ end
+ if config.get_adapter_by_scheme(dest) ~= M then
+ local _, path = util.parse_url(dest)
+ assert(path)
+ dest = files.to_short_os_path(path, action.entry_type)
+ end
+ return string.format(' %s %s -> %s', action.type:upper(), src, dest)
+ else
+ error(string.format("Bad action type: '%s'", action.type))
+ end
+end
+
+---@param src_res oil.ftpUrl
+---@param dest_res oil.ftpUrl
+---@param cb fun(err: nil|string)
+local function ftp_copy_file(src_res, dest_res, cb)
+ local cache_dir = vim.fn.stdpath('cache')
+ assert(type(cache_dir) == 'string')
+ local tmpdir = fs.join(cache_dir, 'oil')
+ fs.mkdirp(tmpdir)
+ local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 'ftp_XXXXXX'))
+ if fd then
+ vim.loop.fs_close(fd)
+ end
+ curl(src_res, { curl_ftp_url(src_res), '-o', tmpfile }, function(err)
+ if err then
+ vim.loop.fs_unlink(tmpfile)
+ return cb(err)
+ end
+ curl(dest_res, { '-T', tmpfile, curl_ftp_url(dest_res) }, function(err2)
+ vim.loop.fs_unlink(tmpfile)
+ cb(err2)
+ end)
+ end)
+end
+
+---@param action oil.Action
+---@param cb fun(err: nil|string)
+M.perform_action = function(action, cb)
+ if action.type == 'create' then
+ local res = M.parse_url(action.url)
+ local ftp_path = ftp_abs_path(res)
+ if action.entry_type == 'directory' then
+ curl(res, {
+ '--quote',
+ string.format('MKD %s', ftp_path),
+ host_ftp_url(res),
+ }, cb)
+ elseif action.entry_type == 'link' then
+ cb('FTP does not support symbolic links')
+ else
+ curl(res, { '-T', '/dev/null', curl_ftp_url(res) }, cb)
+ end
+ elseif action.type == 'delete' then
+ local res = M.parse_url(action.url)
+ local ftp_path = ftp_abs_path(res)
+ if action.entry_type == 'directory' then
+ curl(res, {
+ '--quote',
+ string.format('RMD %s', ftp_path),
+ host_ftp_url(res),
+ }, cb)
+ else
+ curl(res, {
+ '--quote',
+ string.format('DELE %s', ftp_path),
+ host_ftp_url(res),
+ }, cb)
+ end
+ elseif action.type == 'move' then
+ local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
+ local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
+ if src_adapter == M and dest_adapter == M then
+ local src_res = M.parse_url(action.src_url)
+ local dest_res = M.parse_url(action.dest_url)
+ if url_hosts_equal(src_res, dest_res) then
+ curl(src_res, {
+ '--quote',
+ string.format('RNFR %s', ftp_abs_path(src_res)),
+ '--quote',
+ string.format('RNTO %s', ftp_abs_path(dest_res)),
+ host_ftp_url(src_res),
+ }, cb)
+ else
+ if action.entry_type == 'directory' then
+ cb('Cannot move directories across FTP hosts')
+ return
+ end
+ ftp_copy_file(src_res, dest_res, function(err)
+ if err then
+ return cb(err)
+ end
+ curl(src_res, {
+ '--quote',
+ string.format('DELE %s', ftp_abs_path(src_res)),
+ host_ftp_url(src_res),
+ }, cb)
+ end)
+ end
+ else
+ cb('We should never attempt to move across adapters')
+ end
+ elseif action.type == 'copy' then
+ local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
+ local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
+ if src_adapter == M and dest_adapter == M then
+ if action.entry_type == 'directory' then
+ cb('Cannot copy directories over FTP; copy individual files instead')
+ return
+ end
+ local src_res = M.parse_url(action.src_url)
+ local dest_res = M.parse_url(action.dest_url)
+ ftp_copy_file(src_res, dest_res, cb)
+ elseif src_adapter == M then
+ if action.entry_type == 'directory' then
+ cb('Cannot copy FTP directories to local filesystem via curl')
+ return
+ end
+ local src_res = M.parse_url(action.src_url)
+ local _, dest_path = util.parse_url(action.dest_url)
+ assert(dest_path)
+ local local_path = fs.posix_to_os_path(dest_path)
+ curl(src_res, { curl_ftp_url(src_res), '-o', local_path }, cb)
+ else
+ if action.entry_type == 'directory' then
+ cb('Cannot copy local directories to FTP via curl')
+ return
+ end
+ local _, src_path = util.parse_url(action.src_url)
+ assert(src_path)
+ local local_path = fs.posix_to_os_path(src_path)
+ local dest_res = M.parse_url(action.dest_url)
+ curl(dest_res, { '-T', local_path, curl_ftp_url(dest_res) }, cb)
+ end
+ else
+ cb(string.format('Bad action type: %s', action.type))
+ end
+end
+
+M.supported_cross_adapter_actions = { files = 'copy' }
+
+---@param bufnr integer
+M.read_file = function(bufnr)
+ loading.set_loading(bufnr, true)
+ local bufname = vim.api.nvim_buf_get_name(bufnr)
+ local url = M.parse_url(bufname)
+ local basename = pathutil.basename(bufname)
+ local cache_dir = vim.fn.stdpath('cache')
+ assert(type(cache_dir) == 'string')
+ local tmpdir = fs.join(cache_dir, 'oil')
+ fs.mkdirp(tmpdir)
+ local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 'ftp_XXXXXX'))
+ if fd then
+ vim.loop.fs_close(fd)
+ end
+ local tmp_bufnr = vim.fn.bufadd(tmpfile)
+
+ curl(url, { curl_ftp_url(url), '-o', 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
+ local filetype = vim.filetype.match({ buf = bufnr, filename = basename })
+ if filetype then
+ vim.bo[bufnr].filetype = filetype
+ end
+ 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)
+ local url = M.parse_url(bufname)
+ local cache_dir = vim.fn.stdpath('cache')
+ assert(type(cache_dir) == 'string')
+ local tmpdir = fs.join(cache_dir, 'oil')
+ local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 'ftp_XXXXXXXX'))
+ if fd then
+ vim.loop.fs_close(fd)
+ end
+ vim.cmd.doautocmd({ args = { 'BufWritePre', bufname }, mods = { silent = true } })
+ vim.bo[bufnr].modifiable = false
+ vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } })
+ local tmp_bufnr = vim.fn.bufadd(tmpfile)
+
+ curl(url, { '-T', tmpfile, curl_ftp_url(url) }, function(err)
+ vim.bo[bufnr].modifiable = true
+ if err then
+ vim.notify(string.format('Error writing file: %s', err), vim.log.levels.ERROR)
+ else
+ vim.bo[bufnr].modified = false
+ vim.cmd.doautocmd({ args = { 'BufWritePost', bufname }, mods = { silent = true } })
+ end
+ vim.loop.fs_unlink(tmpfile)
+ vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
+ end)
+end
+
+return M
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index ec381b4..f96239e 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -116,6 +116,8 @@ local default_config = {
extra_scp_args = {},
-- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
extra_s3_args = {},
+ -- Extra arguments to pass to curl for FTP operations
+ extra_curl_args = {},
-- EXPERIMENTAL support for performing file operations with git
git = {
-- Return true to automatically git add/mv/rm files
@@ -223,6 +225,8 @@ default_config.adapters = {
['oil-ssh://'] = 'ssh',
[oil_s3_string] = 's3',
['oil-trash://'] = 'trash',
+ ['oil-ftp://'] = 'ftp',
+ ['oil-ftps://'] = 'ftp',
}
default_config.adapter_aliases = {}
-- We want the function in the default config for documentation generation, but if we nil it out
@@ -254,6 +258,7 @@ default_config.view_options.highlight_filename = nil
---@field new_dir_mode integer
---@field extra_scp_args string[]
---@field extra_s3_args string[]
+---@field extra_curl_args string[]
---@field git oil.GitOptions
---@field float oil.FloatWindowConfig
---@field preview_win oil.PreviewWindowConfig
@@ -288,6 +293,7 @@ local M = {}
---@field new_dir_mode? integer Permission mode for new directories in decimal (default 493 = 0755)
---@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH
---@field extra_s3_args? string[] Extra arguments to pass to aws s3 when moving/copying files using aws s3
+---@field extra_curl_args? string[] Extra arguments to pass to curl for FTP operations
---@field git? oil.SetupGitOptions EXPERIMENTAL support for performing file operations with git
---@field float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float
---@field preview_win? oil.SetupPreviewWindowConfig Configuration for the file preview window