canola.nvim/doc/upstream.md
Barrett Ruth a410507846
feat(ftp): add FTP/FTPS adapter via curl (#167)
* feat(ftp): add FTP/FTPS adapter via curl

Problem: canola has no way to browse or edit files on FTP servers,
despite the adapter system being designed for exactly this pattern.
curl speaks FTP natively, including FTPS (FTP over TLS), and requires
no new dependencies.

Solution: implement `lua/oil/adapters/ftp.lua` with `oil-ftp://` and
`oil-ftps://` schemes. Parses Unix and IIS LIST output, supports
`size`, `mtime`, and `permissions` columns, and implements the full
adapter API (list, read_file, write_file, render_action, perform_action).
Same-host renames use RNFR/RNTO; cross-host and local↔FTP copies use
curl download/upload through a tmpfile. Adds `extra_curl_args` config
option and documents the adapter in `doc/oil.txt`.

Based on: stevearc/oil.nvim#210

* docs(upstream): mark #210 fixed in #167

* fix(ftp): use python3 ftplib for control-channel FTP operations

Problem: DELE, RMD, MKD, and RNFR/RNTO were implemented using
curl --quote, which requires a subsequent LIST or STOR to trigger
the FTP connection. That data-channel operation hangs on slow or
busy servers, making every mutation appear stuck.

Solution: replace the curl --quote approach with a python3 ftplib
one-liner for all control-channel operations. ftplib executes DELE,
RMD, MKD, RNFR/RNTO, and SITE CHMOD without opening a data channel,
so they complete instantly. The curl wrapper is retained for LIST,
read_file, and write_file, which genuinely need a data channel.

* fix(ftp): use nil entry ID so cache assigns unique IDs

Problem: `M.list` returned entries as `{0, name, type, meta}`.
`cache.store_entry` only assigns a fresh ID when `entry[FIELD_ID] == nil`;
passing 0 caused every entry to be stored as ID 0, all overwriting
each other. `get_entry_by_id(0)` then always returned the last-stored
entry, breaking navigation (always opened the same file), rename
(wrong entry matched), and create (wrong diff).

Solution: change the placeholder from 0 to nil, matching how
`cache.create_entry` itself builds entries.

* fix(ftp): use ftp.rename() for RNFR/RNTO and raw Python lines in ftpcmd

Problem: `ftpcmd` wrapped every command in `ftp.voidcmd()`, which
expects a final 2xx response. `RNFR` returns 350 (intermediate),
so `voidcmd` raised an exception before `RNTO` was ever sent,
causing every rename to fail with '350 Ready for destination name'.

Solution: change `ftpcmd` to accept raw Python lines instead of FTP
command strings, then use `ftp.rename(src, dst)` for the rename case.
`ftplib.rename` handles the 350 intermediate response correctly
internally. All other callers now wrap their FTP commands in
`ftp.voidcmd()` explicitly.

* fix(ftp): recursively delete directory contents before RMD

Problem: FTP's RMD command fails with '550 Directory not empty'
if the directory has any contents. Unlike the S3 adapter which uses
`aws s3 rm --recursive`, FTP has no protocol-level recursive delete.

Solution: emit a Python rmtree helper inside the ftpcmd script that
walks the directory via MLSD, recursively deletes children (DELE for
files, rmtree for subdirs), then sends RMD on the now-empty directory.

* fix(ftp): give oil-ftps:// its own adapter name to prevent scheme clobbering

Problem: both oil-ftp:// and oil-ftps:// mapped to the adapter name
'ftp', so config.adapter_to_scheme['ftp'] was set to whichever scheme
pairs() iterated last — non-deterministic. init.lua uses
adapter_to_scheme[adapter.name] to reconstruct the parent URL, so
roughly half the time it injected 'oil-ftps://' into ftp:// buffer
navigation, causing the ssl-reqd error on '-' press.

Solution: register oil-ftps:// under adapter name 'ftps' via a
one-line shim that returns require('oil.adapters.ftp'). Now
adapter_to_scheme['ftp'] = 'oil-ftp://' and
adapter_to_scheme['ftps'] = 'oil-ftps://' are both stable.

* fix(ftp): percent-encode path in curl FTP URLs

Problem: filenames containing spaces (or other URL-unsafe characters)
caused curl to fail with "Unknown error" because the raw path was
concatenated directly into the FTP URL.

Solution: add `url_encode_path` to encode non-safe characters (excluding
`/`) before building the curl URL in `curl_ftp_url`.

* fix(ftp): fix STARTTLS, error visibility, and robustness

Problem: `curl_ftp_url` emitted `ftps://` (implicit TLS) for
`oil-ftps://` URLs, causing listing to fail against STARTTLS servers
while Python mutations worked — the two paths spoke different TLS
modes. curl's `-s` flag silenced all error output, producing "Unknown
error" on any curl failure. File creation used `/dev/null` (breaks on
Windows, diverges from S3). The Python TLS context didn't honour
`--insecure`/`-k` from `extra_curl_args`. Deleting non-empty dirs on
servers without MLSD gave a cryptic `500 Unknown command`.

Solution: Always emit `ftp://` in `curl_ftp_url`; TLS is enforced
solely via `--ssl-reqd`, making STARTTLS consistent between curl and
Python. Add `-S` to expose curl errors. Replace `/dev/null` with
`curl -T -` + `stdin='null'` (matches `s3fs` pattern). Mirror
`--insecure`/`-k` into the Python SSL context. Wrap `mlsd()` in
try/except with a clear actionable message. Add `spec/ftp_spec.lua`
with 28 unit tests covering URL parsing, list parsing, and curl URL
building. Update `doc/oil.txt` to document STARTTLS and MLSD.

* ci: format

* fix(ftp): resolve LuaLS type warnings in `curl` wrapper and `parse_unix_list_line`
2026-03-17 23:47:20 -04:00

39 KiB

Upstream Tracker

Triage of stevearc/oil.nvim PRs and issues against this fork.

Upstream PRs

PR Description Status
#495 Cancel visual/operator-pending mode on close cherry-picked
#537 Configurable file/directory creation permissions cherry-picked
#618 Opt-in filetype detection for icons cherry-picked
#644 Pass entry to is_hidden_file/is_always_hidden cherry-picked
#697 Recipe for file extension column cherry-picked
#698 Executable file highlighting cherry-picked
#717 Add oil-git.nvim to extensions cherry-picked
#720 Gate BufAdd autocmd behind config check cherry-picked
#722 Fix freedesktop trash URL cherry-picked
#723 Emit OilReadPost event after render cherry-picked
#725 Normalize keymap keys before config merge cherry-picked
#727 Clarify get_current_dir nil + Telescope recipe cherry-picked
#739 macOS FreeDesktop trash recipe cherry-picked
#488 Parent directory in a split not actionable — empty PR
#493 UNC paths on Windows not actionable — superseded by #686
#686 Windows path conversion fix not actionable — Windows-only
#735 gX opens external program with selection not actionable — hardcoded Linux-only, incomplete
#591 release-please changelog not applicable
#667 Virtual text columns + headers consolidated into #142
#708 Move file into new dir by renaming consolidated into #32
#721 create_hook to populate file contents not actionable — OilFileCreated event already covers the use case (see #280)
#728 open_split for opening oil in a split deferred — tracked as #2

Issues

Issue Description Status
#85 Git status column consolidated into #121
#95 Undo after renaming files open
#117 Move file into new dir via slash in name consolidated into #32
#156 Paste path of files into oil buffer fixed — added oil-recipe-paste-file-from-clipboard
#200 Highlights not working when opening a file not actionable — cannot reproduce, nvim 0.9.4
#207 Suppress "no longer available" message fixed — cleanup_buffers_on_delete option
#210 FTP support fixed (#167)
#213 Disable preview for large files fixed (#85)
#226 K8s/Docker adapter not actionable — no demand
#232 Cannot close last window consolidated into #149
#254 Buffer modified highlight group tracked in #129
#263 Diff mode open
#276 Archives manipulation not actionable — nvim has builtin zip support
#280 vim-projectionist support not actionable — OilFileCreated event provides the correct hook; recipe added to docs
#288 Oil failing to load not actionable — no reliable repro, likely lazy.nvim timing
#289 Show absolute path toggle not actionable — display solved by get_win_title, editing consolidated into #32
#294 Can't handle emojis in filenames not actionable — libuv bug (nodejs/node#49042)
#298 Open float on neovim directory startup open
#302 buflisted=true after jumplist nav fixed (#71)
#303 Preview in float window mode fixed — upstream #403, config.float.preview_split
#325 oil-ssh error from command line consolidated into #164
#330 Telescope opens file in oil float not actionable — cross-plugin, no repro
#332 Buffer not fixed to floating window not actionable — cannot reproduce
#335 Disable editing outside root dir not actionable — confirmation prompt and delete_to_trash already cover accidental deletion
#349 Parent directory as column/vsplit not actionable — different navigation paradigm, use mini.files
#351 Paste deleted file from register not actionable — workflow issue (move before saving), confirmation prompt and delete_to_trash cover recovery
#359 Parse error on filenames differing by space not actionable — parser uses whitespace as column delimiter
#360 Pick window to open file into open
#362 "Could not find oil adapter for scheme" not actionable — no repro, old nvim (0.9.5)
#363 prompt_save_on_select_new_entry wrong prompt fixed
#371 Constrain cursor in insert mode fixed (#93)
#373 Dir from quickfix with bqf/trouble broken open
#375 Highlights for file types and permissions fixed — per-character permission column highlights
#380 Silently overriding show_hidden not actionable — counter to config intent
#382 Relative path in window title not actionable — solved by get_win_title callback (#482)
#392 Option to skip delete prompt fixed
#393 Auto-save on select fixed
#396 Customize preview content not actionable — out of scope, preview is a normal buffer; use BufReadCmd autocmds for custom renderers
#399 Open file without closing Oil fixed (#159)
#404 Restricted UNC paths not actionable — Windows-only
#416 Cannot remap key to open split fixed — cherry-picked (#725)
#431 More SSH adapter documentation duplicate of #525
#435 Error previewing with semantic tokens LSP fixed — cherry-picked (#467)
#436 Owner and group columns consolidated into #126
#444 Opening behaviour customization not actionable — existing API covers all use cases, reporter satisfied
#446 Executable highlighting cherry-picked (#698)
#449 Renaming TypeScript files stopped working not actionable — config issue, increase lsp_file_methods.timeout_ms
#450 Highlight opened file in directory listing fixed — added oil-recipe-highlight-opened-file
#457 Custom column API open
#466 Select into window on right not actionable — user-land concern, custom action trivially solves this
#473 Show hidden when dir is all-hidden fixed (#85)
#479 Harpoon integration recipe not actionable — no demand, stale
#483 Spell downloads depend on netrw not actionable — fixed in neovim#34940
#486 Directory sizes show misleading 4.1k fixed (#87)
#492 j/k remapping question not actionable — answered
#507 lacasitos.nvim conflict not actionable — cross-plugin + Windows-only
#521 oil-ssh connection issues open
#525 SSH adapter documentation fixed — expanded :help oil-adapter-ssh
#531 Incomplete drive letters not actionable — Windows-only
#533 constrain_cursor bug not actionable — needs repro
#570 Improve c0/d0 for renaming not actionable — blocked on Neovim extmark API
#571 Callback before highlight_filename not actionable — solved by existing extensions (oil-git-status.nvim, oil-git.nvim)
#578 Hidden file dimming recipe fixed
#587 Alt+h keymap not actionable — user config issue
#599 user:group display and manipulation consolidated into #126
#607 Per-host SCP args open
#609 Cursor placement via Snacks picker not actionable — Windows-only
#612 Delete buffers on file delete fixed
#615 Cursor at name column on o/O fixed (#72)
#617 Filetype by actual filetype fixed — cherry-picked (#618)
#621 toggle() for regular windows fixed (#88)
#623 bufferline.nvim interaction open
#624 Mutation race not actionable — no reliable repro
#625 E19 mark invalid line not actionable — intractable without neovim API changes
#632 Preview + move = copy fixed (#12)
#636 Telescope picker opens in active buffer not actionable — cannot reproduce
#637 Inconsistent symlink resolution not actionable — nightly-only, no stable repro
#641 Flicker on actions.parent open
#642 W10 warning under nvim -R fixed
#645 close_float action fixed
#646 get_current_dir nil on SSH fixed — get_current_url() API
#650 LSP workspace.fileOperations events fixed
#655 File statistics as virtual text consolidated into #142
#659 Mark and diff files in buffer open
#664 Session reload extra buffer consolidated into #149
#665 Hot load preview fast-scratch buffers not actionable — no clear architecture
#668 Custom yes/no confirmation not actionable — no demand
#670 Multi-directory cmdline args ignored fixed (#11)
#671 Yanking between nvim instances not actionable — addressed upstream by clipboard actions
#673 Symlink newlines crash fixed
#675 Move file into folder by renaming consolidated into #32
#676 Windows path conversion not actionable — Windows-only
#678 buftype='acwrite' causes mksession to skip oil windows consolidated into #149
#679 Executable file sign cherry-picked (#698)
#682 get_current_dir() nil cherry-picked (#727)
#683 Path not shown in floating mode fixed
#684 User and group columns consolidated into #126
#685 Plain directory paths in buffer names not actionable — protocol prefix is fundamental to buffer identity
#690 OilFileIcon highlight group fixed
#692 Keymap normalization cherry-picked (#725)
#699 select blocks UI with slow FileType autocmd fixed (#106)
#707 Move file/dir into new dir by renaming consolidated into #32
#710 buftype empty on BufEnter fixed (#10)
#714 Support question not actionable — answered
#719 Neovim crash on node_modules not actionable — libuv/neovim bug
#726 oil.nvim in the state of 2026 not actionable — meta discussion/roadmap
#736 Make icons virtual text consolidated into #142
#738 Allow changing mtime/atime via time column not actionable — disproportionate complexity (new mutator action type, reverse strftime parser, per-adapter utime), 0 demand, purpose-built tools exist