diff --git a/.envrc b/.envrc index d522e34..175de89 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1 @@ -export VIRTUAL_ENV=venv layout python -python -c 'import pyparsing' 2>/dev/null || pip install -r scripts/requirements.txt diff --git a/.github/workflows/install_nvim.sh b/.github/workflows/install_nvim.sh index 9ba2d26..4c0203c 100644 --- a/.github/workflows/install_nvim.sh +++ b/.github/workflows/install_nvim.sh @@ -1,16 +1,12 @@ #!/bin/bash set -e -version="${NVIM_TAG-stable}" -dl_name="nvim-linux-x86_64.appimage" -# The appimage name changed in v0.10.4 -if python -c 'from packaging.version import Version; import sys; sys.exit(not (Version(sys.argv[1]) < Version("v0.10.4")))' "$version" 2>/dev/null; then - dl_name="nvim.appimage" -fi -curl -sL "https://github.com/neovim/neovim/releases/download/${version}/${dl_name}" -o nvim.appimage +PLUGINS="$HOME/.local/share/nvim/site/pack/plugins/start" +mkdir -p "$PLUGINS" + +wget "https://github.com/neovim/neovim/releases/download/${NVIM_TAG-stable}/nvim.appimage" chmod +x nvim.appimage ./nvim.appimage --appimage-extract >/dev/null rm -f nvim.appimage mkdir -p ~/.local/share/nvim mv squashfs-root ~/.local/share/nvim/appimage sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim -/usr/bin/nvim --version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b62853..08957e5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - stevearc-* pull_request: branches: - master @@ -35,7 +34,7 @@ jobs: uses: JohnnyMorganz/stylua-action@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v2.0.2 + version: v0.20.0 args: --check lua tests typecheck: @@ -53,8 +52,7 @@ jobs: include: - nvim_tag: v0.8.3 - nvim_tag: v0.9.4 - - nvim_tag: v0.10.4 - - nvim_tag: v0.11.0 + - nvim_tag: v0.10.0 name: Run tests runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index d427c40..d8cb86e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,6 @@ luac.out *.zip *.tar.gz -# python bytecode -__pycache__ - # Object files *.o *.os @@ -44,10 +41,6 @@ __pycache__ .direnv/ .testenv/ -venv/ doc/tags scripts/nvim_doc_tools scripts/nvim-typecheck-action -scripts/benchmark.nvim -perf/tmp/ -profile.json diff --git a/.luarc.json b/.luarc.json deleted file mode 100644 index 68da2f2..0000000 --- a/.luarc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "runtime": { - "version": "LuaJIT", - "pathStrict": true - }, - "type": { - "checkTableShape": true - } -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 458b3cb..39811f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,86 +1,5 @@ # Changelog -## [2.15.0](https://github.com/stevearc/oil.nvim/compare/v2.14.0...v2.15.0) (2025-02-13) - - -### Features - -* add support for bufnr in column rendering functions ([#575](https://github.com/stevearc/oil.nvim/issues/575)) ([8abc58b](https://github.com/stevearc/oil.nvim/commit/8abc58b038f84078121ab1cac6ecad0163fe1635)) -* API to automatically open preview window after opening oil ([#339](https://github.com/stevearc/oil.nvim/issues/339)) ([57528bf](https://github.com/stevearc/oil.nvim/commit/57528bf9c58080ca891e8d362d0a578895c136ce)) -* can selectively add entries to quickfix ([#564](https://github.com/stevearc/oil.nvim/issues/564)) ([b594b9a](https://github.com/stevearc/oil.nvim/commit/b594b9a9052618669ccf6520b2d0c0d942eb8118)) -* floating window max width/height can be percentages ([#553](https://github.com/stevearc/oil.nvim/issues/553)) ([1df90fa](https://github.com/stevearc/oil.nvim/commit/1df90faf927e78f5aacf278abd0bfdcb5f45e825)) -* most moves and copies will copy the undofile ([#583](https://github.com/stevearc/oil.nvim/issues/583)) ([32dd3e3](https://github.com/stevearc/oil.nvim/commit/32dd3e378d47673679e76a773451f82f971a66df)) -* pass oil bufnr to custom filename highlight function ([#552](https://github.com/stevearc/oil.nvim/issues/552)) ([f5c563a](https://github.com/stevearc/oil.nvim/commit/f5c563a074a38cee5a09f98e98b74dcd2c322490)) - - -### Bug Fixes - -* crash in preview on nvim 0.8 ([81b2c5f](https://github.com/stevearc/oil.nvim/commit/81b2c5f04ae24a8c83b20ecbd017fecac15faca0)) -* directory rendering with custom highlights ([#551](https://github.com/stevearc/oil.nvim/issues/551)) ([a6a4f48](https://github.com/stevearc/oil.nvim/commit/a6a4f48b14b4a51fded531c86f6c04b4503a2ef8)) -* disable_preview respected when preview_method != "load" ([#577](https://github.com/stevearc/oil.nvim/issues/577)) ([7cde5aa](https://github.com/stevearc/oil.nvim/commit/7cde5aab10f564408e9ac349d457d755422d58cd)) -* error when non-current oil buffer has validation errors ([#561](https://github.com/stevearc/oil.nvim/issues/561)) ([8d11a2a](https://github.com/stevearc/oil.nvim/commit/8d11a2abf3039b1974d4acd65fbc83ada2ca1084)) -* gracefully handle fs_stat failures ([#558](https://github.com/stevearc/oil.nvim/issues/558)) ([7c26a59](https://github.com/stevearc/oil.nvim/commit/7c26a59ac0061b199bf9f44b19d45cfadd9b14f5)) -* guard against nil metadata values ([#548](https://github.com/stevearc/oil.nvim/issues/548)) ([254bc66](https://github.com/stevearc/oil.nvim/commit/254bc6635cb3f77e6e9a89155652f368e5535160)) -* more consistent cursor position when entering a new directory ([#536](https://github.com/stevearc/oil.nvim/issues/536)) ([c80fa5c](https://github.com/stevearc/oil.nvim/commit/c80fa5c415b882c1c694a32748cea09b7dafc2c5)) -* more robust parsing of custom column timestamp formats ([#582](https://github.com/stevearc/oil.nvim/issues/582)) ([5313690](https://github.com/stevearc/oil.nvim/commit/5313690956d27cc6b53d5a2583df05e717c59b16)) -* open files in correct window from floating oil ([#560](https://github.com/stevearc/oil.nvim/issues/560)) ([83ac518](https://github.com/stevearc/oil.nvim/commit/83ac5185f79ab8d869bccea792dc516ad02ad06e)) -* preview sometimes causes oil buffers to be stuck in unloaded state ([#563](https://github.com/stevearc/oil.nvim/issues/563)) ([1488f0d](https://github.com/stevearc/oil.nvim/commit/1488f0d96b1cb820dd12f05a7bf5283a631a7c4d)) -* stat files if fs_readdir doesn't provide a type ([#543](https://github.com/stevearc/oil.nvim/issues/543)) ([c6a39a6](https://github.com/stevearc/oil.nvim/commit/c6a39a69b2df7c10466f150dde0bd23e49c1fba3)) -* support permissions checks on windows and virtual filesystems ([#555](https://github.com/stevearc/oil.nvim/issues/555)) ([7041528](https://github.com/stevearc/oil.nvim/commit/7041528bdedb350ad66e650684deec8456e053cc)) -* work around incorrect link detection on windows ([#557](https://github.com/stevearc/oil.nvim/issues/557)) ([09fa1d2](https://github.com/stevearc/oil.nvim/commit/09fa1d22f5edf0730824d2b222d726c8c81bbdc9)) - -## [2.14.0](https://github.com/stevearc/oil.nvim/compare/v2.13.0...v2.14.0) (2024-12-21) - - -### Features - -* add `win_options` to `preview_win` ([#514](https://github.com/stevearc/oil.nvim/issues/514)) ([bbeed86](https://github.com/stevearc/oil.nvim/commit/bbeed86bde134da8d09bed64b6aa0d65642e6b23)) -* add highlight group for orphaned links ([#502](https://github.com/stevearc/oil.nvim/issues/502)) ([740b8fd](https://github.com/stevearc/oil.nvim/commit/740b8fd425a2b77f7f40eb5ac155ebe66ff9515c)) -* better merging of action desc when overriding keymaps ([f2b3249](https://github.com/stevearc/oil.nvim/commit/f2b324933f4d505cff6f7d445fd61fad02dcd9ae)) -* config option to customize filename highlight group ([#508](https://github.com/stevearc/oil.nvim/issues/508)) ([99ce32f](https://github.com/stevearc/oil.nvim/commit/99ce32f4a2ecf76263b72fcc31efb163faa1a941)) -* config option to disable previewing a file ([3fa3161](https://github.com/stevearc/oil.nvim/commit/3fa3161aa9515ff6a7cf7e44458b6a2114262870)) -* disable preview for large files ([#511](https://github.com/stevearc/oil.nvim/issues/511)) ([c23fe08](https://github.com/stevearc/oil.nvim/commit/c23fe08e0546d9efc242e19f0d829efa7e7b2743)) -* highlight groups for hidden files ([#459](https://github.com/stevearc/oil.nvim/issues/459)) ([60e6896](https://github.com/stevearc/oil.nvim/commit/60e68967e51ff1ecd264c29e3de0d52bfff22df3)) -* option to quite vim if oil is closed as last buffer ([#491](https://github.com/stevearc/oil.nvim/issues/491)) ([81cc9c3](https://github.com/stevearc/oil.nvim/commit/81cc9c3f62ddbef3687931d119e505643496fa0a)) -* use scratch buffer for file previews ([#467](https://github.com/stevearc/oil.nvim/issues/467)) ([21705a1](https://github.com/stevearc/oil.nvim/commit/21705a1debe6d85a53c138ab944484b685432b2b)) - - -### Bug Fixes - -* cursor sometimes does not hover previous file ([8ea40b5](https://github.com/stevearc/oil.nvim/commit/8ea40b5506115b6d355e304dd9ee5089f7d78601)) -* don't take over the preview window until it's opened for oil ([#532](https://github.com/stevearc/oil.nvim/issues/532)) ([78ab7ca](https://github.com/stevearc/oil.nvim/commit/78ab7ca1073731ebdf82efa474202defa028d5a4)) -* handle files with newlines in the name ([#534](https://github.com/stevearc/oil.nvim/issues/534)) ([dba0375](https://github.com/stevearc/oil.nvim/commit/dba037598843973b8c54bc5ce0318db4a0da439d)) -* image.nvim previews with preview_method=scratch ([5acab3d](https://github.com/stevearc/oil.nvim/commit/5acab3d8a9bc85a571688db432f2702dd7d901a4)) -* improper file name escaping ([#530](https://github.com/stevearc/oil.nvim/issues/530)) ([7a55ede](https://github.com/stevearc/oil.nvim/commit/7a55ede5e745e31ea8e4cb5483221524922294bf)) -* set alternate when using floating windows ([#526](https://github.com/stevearc/oil.nvim/issues/526)) ([c5f7c56](https://github.com/stevearc/oil.nvim/commit/c5f7c56644425e2b77e71904da98cda0331b3342)) -* work around performance issue with treesitter, folds, and large directories ([da93d55](https://github.com/stevearc/oil.nvim/commit/da93d55e32d73a17c447067d168d80290ae96590)) - - -### Performance Improvements - -* change default view_options.natural_order behavior to disable on large directories ([01b0b9d](https://github.com/stevearc/oil.nvim/commit/01b0b9d8ef79b7b631e92f6b5fed1c639262d570)) -* only sort entries after we have them all ([792f0db](https://github.com/stevearc/oil.nvim/commit/792f0db6ba8b626b14bc127e1ce7247185b3be91)) -* optimize rendering cadence ([c96f93d](https://github.com/stevearc/oil.nvim/commit/c96f93d894cc97e76b0871bec4058530eee8ece4)) -* replace vim.endswith and vim.startswith with string.match ([4de3025](https://github.com/stevearc/oil.nvim/commit/4de30256c32cd272482bc6df0c6de78ffc389153)) - -## [2.13.0](https://github.com/stevearc/oil.nvim/compare/v2.12.2...v2.13.0) (2024-11-11) - - -### Features - -* config option to customize floating window title ([#482](https://github.com/stevearc/oil.nvim/issues/482)) ([5d2dfae](https://github.com/stevearc/oil.nvim/commit/5d2dfae655b9b689bd4017b3bdccd52cbee5b92f)) -* config option to disable lsp file methods ([#477](https://github.com/stevearc/oil.nvim/issues/477)) ([f60bb7f](https://github.com/stevearc/oil.nvim/commit/f60bb7f793477d99ef1acf39e920bf2ca4e644de)) - - -### Bug Fixes - -* actions.preview accepts options ([#497](https://github.com/stevearc/oil.nvim/issues/497)) ([cca1631](https://github.com/stevearc/oil.nvim/commit/cca1631d5ea450c09ba72f3951a9e28105a3632c)) -* add trailing slash to directories on yank_entry ([#504](https://github.com/stevearc/oil.nvim/issues/504)) ([42333bb](https://github.com/stevearc/oil.nvim/commit/42333bb46e34dd47e13927010b1dcd30e6e4ca96)) -* don't deep merge keymaps ([#510](https://github.com/stevearc/oil.nvim/issues/510)) ([709403c](https://github.com/stevearc/oil.nvim/commit/709403ccd6f22d859c2e42c780ab558ae89284d9)) -* guard against nil keymaps ([621f8ba](https://github.com/stevearc/oil.nvim/commit/621f8ba4fa821724e9b646732a26fb2e795fe008)) -* only map ~ for normal mode ([#484](https://github.com/stevearc/oil.nvim/issues/484)) ([ccab9d5](https://github.com/stevearc/oil.nvim/commit/ccab9d5e09e2d0042fbbe5b6bd05e82426247067)) -* sort keymap help entries by description ([#506](https://github.com/stevearc/oil.nvim/issues/506)) ([52cc8a1](https://github.com/stevearc/oil.nvim/commit/52cc8a1fb35ea6ce1df536143add7ce7215c63c0)), closes [#376](https://github.com/stevearc/oil.nvim/issues/376) - ## [2.12.2](https://github.com/stevearc/oil.nvim/compare/v2.12.1...v2.12.2) (2024-09-10) diff --git a/Makefile b/Makefile index 10f01d1..71447f8 100644 --- a/Makefile +++ b/Makefile @@ -1,66 +1,27 @@ -## help: print this help message -.PHONY: help -help: - @echo 'Usage:' - @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' +.PHONY: all doc test lint fastlint clean -## all: generate docs, lint, and run tests -.PHONY: all all: doc lint test -venv: - python3 -m venv venv - venv/bin/pip install -r scripts/requirements.txt +doc: scripts/nvim_doc_tools + python scripts/main.py generate + python scripts/main.py lint -## doc: generate documentation -.PHONY: doc -doc: scripts/nvim_doc_tools venv - venv/bin/python scripts/main.py generate - venv/bin/python scripts/main.py lint - -## test: run tests -.PHONY: test test: ./run_tests.sh -## lint: run linters and LuaLS typechecking -.PHONY: lint lint: scripts/nvim-typecheck-action fastlint ./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua -## fastlint: run only fast linters -.PHONY: fastlint -fastlint: scripts/nvim_doc_tools venv - venv/bin/python scripts/main.py lint +fastlint: scripts/nvim_doc_tools + python scripts/main.py lint luacheck lua tests --formatter plain stylua --check lua tests -## profile: use LuaJIT profiler to profile the plugin -.PHONY: profile -profile: scripts/benchmark.nvim - nvim --clean -u perf/bootstrap.lua -c 'lua jit_profile()' - -## flame_profile: create a trace in the chrome profiler format -.PHONY: flame_profile -flame_profile: scripts/benchmark.nvim - nvim --clean -u perf/bootstrap.lua -c 'lua flame_profile()' - -## benchmark: benchmark performance opening directory with many files -.PHONY: benchmark -benchmark: scripts/benchmark.nvim - nvim --clean -u perf/bootstrap.lua -c 'lua benchmark()' - @cat perf/tmp/benchmark.txt - scripts/nvim_doc_tools: git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools scripts/nvim-typecheck-action: git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action -scripts/benchmark.nvim: - git clone https://github.com/stevearc/benchmark.nvim scripts/benchmark.nvim - -## clean: reset the repository to a clean state -.PHONY: clean clean: - rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv perf/tmp profile.json + rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action diff --git a/README.md b/README.md index bf12bed..0728e9f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94 - [Options](#options) - [Adapters](#adapters) - [Recipes](#recipes) -- [Third-party extensions](#third-party-extensions) - [API](#api) - [FAQ](#faq) @@ -22,7 +21,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94 - Neovim 0.8+ - Icon provider plugin (optional) - - [mini.icons](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-icons.md) for file and folder icons + - [mini.icons](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-icons.md) for file and folder icons - [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons ## Installation @@ -39,10 +38,8 @@ oil.nvim supports all the usual plugin managers ---@type oil.SetupOpts opts = {}, -- Optional dependencies - dependencies = { { "nvim-mini/mini.icons", opts = {} } }, - -- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if you prefer nvim-web-devicons - -- Lazy loading is not recommended because it is very tricky to make it work correctly in all situations. - lazy = false, + dependencies = { { "echasnovski/mini.icons", opts = {} } }, + -- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if prefer nvim-web-devicons } ``` @@ -173,8 +170,6 @@ require("oil").setup({ -- Note that the cleanup process only starts when none of the oil buffers are currently displayed cleanup_delay_ms = 2000, lsp_file_methods = { - -- Enable or disable LSP file operations - enabled = true, -- Time to wait for LSP file operations to complete before skipping timeout_ms = 1000, -- Set to true to autosave buffers that are updated with LSP willRenameFiles @@ -193,22 +188,22 @@ require("oil").setup({ -- Set to `false` to remove a keymap -- See :help oil-actions for a list of all available actions keymaps = { - ["g?"] = { "actions.show_help", mode = "n" }, + ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select", opts = { vertical = true } }, - [""] = { "actions.select", opts = { horizontal = true } }, - [""] = { "actions.select", opts = { tab = true } }, + [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" }, + [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" }, + [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" }, [""] = "actions.preview", - [""] = { "actions.close", mode = "n" }, + [""] = "actions.close", [""] = "actions.refresh", - ["-"] = { "actions.parent", mode = "n" }, - ["_"] = { "actions.open_cwd", mode = "n" }, - ["`"] = { "actions.cd", mode = "n" }, - ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" }, - ["gs"] = { "actions.change_sort", mode = "n" }, + ["-"] = "actions.parent", + ["_"] = "actions.open_cwd", + ["`"] = "actions.cd", + ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" }, + ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", - ["g."] = { "actions.toggle_hidden", mode = "n" }, - ["g\\"] = { "actions.toggle_trash", mode = "n" }, + ["g."] = "actions.toggle_hidden", + ["g\\"] = "actions.toggle_trash", }, -- Set to false to disable all of the above keymaps use_default_keymaps = true, @@ -217,16 +212,15 @@ require("oil").setup({ show_hidden = false, -- This function defines what is considered a "hidden" file is_hidden_file = function(name, bufnr) - local m = name:match("^%.") - return m ~= nil + return vim.startswith(name, ".") end, -- This function defines what will never be shown, even when `show_hidden` is set is_always_hidden = function(name, bufnr) return false end, - -- Sort file names with numbers in a more intuitive order for humans. - -- Can be "fast", true, or false. "fast" will turn it off for large directories. - natural_order = "fast", + -- Sort file names in a more intuitive order for humans. Is less performant, + -- so you may want to set to false if you work with large directories. + natural_order = true, -- Sort file and directory names case insensitive case_insensitive = false, sort = { @@ -235,15 +229,9 @@ require("oil").setup({ { "type", "asc" }, { "name", "asc" }, }, - -- Customize the highlight group for the file name - highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan) - return nil - end, }, -- Extra arguments to pass to SCP when moving/copying files over SSH extra_scp_args = {}, - -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3 - extra_s3_args = {}, -- EXPERIMENTAL support for performing file operations with git git = { -- Return true to automatically git add/mv/rm files @@ -261,15 +249,12 @@ require("oil").setup({ float = { -- Padding around the floating window padding = 2, - -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) max_width = 0, max_height = 0, - border = nil, + border = "rounded", win_options = { winblend = 0, }, - -- optionally override the oil buffers window title with custom function: fun(winid: integer): string - get_win_title = nil, -- preview_split: Split direction: "auto", "left", "right", "above", "below". preview_split = "auto", -- This is the config that will be passed to nvim_open_win. @@ -278,21 +263,8 @@ require("oil").setup({ return conf end, }, - -- Configuration for the file preview window - preview_win = { - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, - -- How to open the preview window "load"|"scratch"|"fast_scratch" - preview_method = "fast_scratch", - -- A function that returns true to disable preview on a file e.g. to avoid lag - disable_preview = function(filename) - return false - end, - -- Window-local options to use for preview window buffers - win_options = {}, - }, - -- Configuration for the floating action confirmation window - confirmation = { + -- Configuration for the actions floating preview window + preview = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -309,10 +281,12 @@ require("oil").setup({ min_height = { 5, 0.1 }, -- optionally define an integer/float for the exact height of the preview window height = nil, - border = nil, + border = "rounded", win_options = { winblend = 0, }, + -- Whether the preview window is automatically updated when the cursor is moved + update_on_cursor_moved = true, }, -- Configuration for the floating progress window progress = { @@ -322,7 +296,7 @@ require("oil").setup({ max_height = { 10, 0.9 }, min_height = { 5, 0.1 }, height = nil, - border = nil, + border = "rounded", minimized_border = "none", win_options = { winblend = 0, @@ -330,11 +304,11 @@ require("oil").setup({ }, -- Configuration for the floating SSH window ssh = { - border = nil, + border = "rounded", }, -- Configuration for the floating keymaps help window keymaps_help = { - border = nil, + border = "rounded", }, }) ``` @@ -357,30 +331,11 @@ This may look familiar. In fact, this is the 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/sh` binary as well as standard unix commands (`ls`, `rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`). -### S3 - -This adapter allows you to browse files stored in aws s3. To use it, make sure `aws` is setup correctly and then simply open a buffer using the following name template: - -``` -nvim oil-s3://[bucket]/[path] -``` - -Note that older versions of Neovim don't support numbers in the url, so for Neovim 0.11 and older the url starts with `oil-sss`. - ## Recipes - [Toggle file detail view](doc/recipes.md#toggle-file-detail-view) -- [Show CWD in the winbar](doc/recipes.md#show-cwd-in-the-winbar) - [Hide gitignored files and show git tracked hidden files](doc/recipes.md#hide-gitignored-files-and-show-git-tracked-hidden-files) -## Third-party extensions - -These are plugins maintained by other authors that extend the functionality of oil.nvim. - -- [oil-git-status.nvim](https://github.com/refractalize/oil-git-status.nvim) - Shows git status of files in statuscolumn -- [oil-git.nvim](https://github.com/benomahony/oil-git.nvim) - Shows git status of files with colour and symbols -- [oil-lsp-diagnostics.nvim](https://github.com/JezerM/oil-lsp-diagnostics.nvim) - Shows LSP diagnostics indicator as virtual text - ## API @@ -393,11 +348,11 @@ These are plugins maintained by other authors that extend the functionality of o - [set_is_hidden_file(is_hidden_file)](doc/api.md#set_is_hidden_fileis_hidden_file) - [toggle_hidden()](doc/api.md#toggle_hidden) - [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr) -- [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb) -- [toggle_float(dir, opts, cb)](doc/api.md#toggle_floatdir-opts-cb) -- [open(dir, opts, cb)](doc/api.md#opendir-opts-cb) -- [close(opts)](doc/api.md#closeopts) -- [open_preview(opts, callback)](doc/api.md#open_previewopts-callback) +- [open_float(dir)](doc/api.md#open_floatdir) +- [toggle_float(dir)](doc/api.md#toggle_floatdir) +- [open(dir)](doc/api.md#opendir) +- [close()](doc/api.md#close) +- [open_preview(opts)](doc/api.md#open_previewopts) - [select(opts, callback)](doc/api.md#selectopts-callback) - [save(opts, cb)](doc/api.md#saveopts-cb) - [setup(opts)](doc/api.md#setupopts) @@ -421,7 +376,7 @@ Plus, I think it's pretty slick ;) - You like to use a netrw-like view to browse directories (as opposed to a file tree) - AND you want to be able to edit your filesystem like a buffer -- AND you want to perform cross-directory actions. AFAIK there is no other plugin that does this. (update: [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md) also offers this functionality) +- AND you want to perform cross-directory actions. AFAIK there is no other plugin that does this. (update: [mini.files](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-files.md) also offers this functionality) If you don't need those features specifically, check out the alternatives listed below @@ -437,7 +392,7 @@ If you don't need those features specifically, check out the alternatives listed **A:** -- [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md): A newer plugin that also supports cross-directory filesystem-as-buffer edits. It utilizes a unique column view. +- [mini.files](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-files.md): A newer plugin that also supports cross-directory filesystem-as-buffer edits. It utilizes a unique column view. - [vim-vinegar](https://github.com/tpope/vim-vinegar): The granddaddy. This made me fall in love with single-directory file browsing. I stopped using it when I encountered netrw bugs and performance issues. - [defx.nvim](https://github.com/Shougo/defx.nvim): What I switched to after vim-vinegar. Much more flexible and performant, but requires python and the API is a little hard to work with. - [dirbuf.nvim](https://github.com/elihunter173/dirbuf.nvim): The first plugin I encountered that let you edit the filesystem like a buffer. Never used it because it [can't do cross-directory edits](https://github.com/elihunter173/dirbuf.nvim/issues/7). diff --git a/doc/api.md b/doc/api.md index 24d4a50..e107293 100644 --- a/doc/api.md +++ b/doc/api.md @@ -10,11 +10,11 @@ - [set_is_hidden_file(is_hidden_file)](#set_is_hidden_fileis_hidden_file) - [toggle_hidden()](#toggle_hidden) - [get_current_dir(bufnr)](#get_current_dirbufnr) -- [open_float(dir, opts, cb)](#open_floatdir-opts-cb) -- [toggle_float(dir, opts, cb)](#toggle_floatdir-opts-cb) -- [open(dir, opts, cb)](#opendir-opts-cb) -- [close(opts)](#closeopts) -- [open_preview(opts, callback)](#open_previewopts-callback) +- [open_float(dir)](#open_floatdir) +- [toggle_float(dir)](#toggle_floatdir) +- [open(dir)](#opendir) +- [close()](#close) +- [open_preview(opts)](#open_previewopts) - [select(opts, callback)](#selectopts-callback) - [save(opts, cb)](#saveopts-cb) - [setup(opts)](#setupopts) @@ -92,100 +92,76 @@ Get the current directory | ----- | -------------- | ---- | | bufnr | `nil\|integer` | | -## open_float(dir, opts, cb) +## open_float(dir) -`open_float(dir, opts, cb)` \ +`open_float(dir)` \ Open oil browser in a floating window -| Param | Type | Desc | -| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -| opts | `nil\|oil.OpenOpts` | | -| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil | -| >>vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| cb | `nil\|fun()` | Called after the oil buffer is ready | +| Param | Type | Desc | +| ----- | ------------- | ------------------------------------------------------------------------------------------- | +| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -## toggle_float(dir, opts, cb) +## toggle_float(dir) -`toggle_float(dir, opts, cb)` \ +`toggle_float(dir)` \ Open oil browser in a floating window, or close it if open -| Param | Type | Desc | -| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -| opts | `nil\|oil.OpenOpts` | | -| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil | -| >>vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| cb | `nil\|fun()` | Called after the oil buffer is ready | +| Param | Type | Desc | +| ----- | ------------- | ------------------------------------------------------------------------------------------- | +| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -## open(dir, opts, cb) +## open(dir) -`open(dir, opts, cb)` \ +`open(dir)` \ Open oil browser for a directory -| Param | Type | Desc | -| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -| opts | `nil\|oil.OpenOpts` | | -| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil | -| >>vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| cb | `nil\|fun()` | Called after the oil buffer is ready | +| Param | Type | Desc | +| ----- | ------------- | ------------------------------------------------------------------------------------------- | +| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -## close(opts) +## close() -`close(opts)` \ +`close()` \ Restore the buffer that was present when oil was opened -| Param | Type | Desc | -| ----------------- | -------------------- | --------------------------------------------------- | -| opts | `nil\|oil.CloseOpts` | | -| >exit_if_last_buf | `nil\|boolean` | Exit vim if this oil buffer is the last open buffer | -## open_preview(opts, callback) +## open_preview(opts) -`open_preview(opts, callback)` \ +`open_preview(opts)` \ Preview the entry under the cursor in a split -| Param | Type | Desc | -| ----------- | ------------------------------------------------------- | ---------------------------------------------- | -| opts | `nil\|oil.OpenPreviewOpts` | | -| >vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| callback | `nil\|fun(err: nil\|string)` | Called once the preview window has been opened | +| Param | Type | Desc | | +| ----- | ------------ | -------------------------------------------------- | ------------------------------------- | +| opts | `nil\|table` | | | +| | vertical | `boolean` | Open the buffer in a vertical split | +| | horizontal | `boolean` | Open the buffer in a horizontal split | +| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | ## select(opts, callback) `select(opts, callback)` \ Select the entry under the cursor -| Param | Type | Desc | -| ----------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| opts | `nil\|oil.SelectOpts` | | -| >vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| >tab | `nil\|boolean` | Open the buffer in a new tab | -| >close | `nil\|boolean` | Close the original oil buffer once selection is made | -| >handle_buffer_callback | `nil\|fun(buf_id: integer)` | If defined, all other buffer related options here would be ignored. This callback allows you to take over the process of opening the buffer yourself. | -| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | +| Param | Type | Desc | | +| -------- | ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------- | +| opts | `nil\|oil.SelectOpts` | | | +| | vertical | `nil\|boolean` | Open the buffer in a vertical split | +| | horizontal | `nil\|boolean` | Open the buffer in a horizontal split | +| | split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| | tab | `nil\|boolean` | Open the buffer in a new tab | +| | close | `nil\|boolean` | Close the original oil buffer once selection is made | +| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | | ## save(opts, cb) `save(opts, cb)` \ Save all changes -| Param | Type | Desc | -| -------- | ---------------------------- | ------------------------------------------------------------------------------------------- | -| opts | `nil\|table` | | -| >confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | -| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. | +| Param | Type | Desc | | +| ----- | ---------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------- | +| opts | `nil\|table` | | | +| | confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | +| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. | | **Note:**
diff --git a/doc/oil.txt b/doc/oil.txt
index 8753f87..1877be3 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -55,8 +55,6 @@ CONFIG                                                                *oil-confi
       -- Note that the cleanup process only starts when none of the oil buffers are currently displayed
       cleanup_delay_ms = 2000,
       lsp_file_methods = {
-        -- Enable or disable LSP file operations
-        enabled = true,
         -- Time to wait for LSP file operations to complete before skipping
         timeout_ms = 1000,
         -- Set to true to autosave buffers that are updated with LSP willRenameFiles
@@ -75,22 +73,22 @@ CONFIG                                                                *oil-confi
       -- Set to `false` to remove a keymap
       -- See :help oil-actions for a list of all available actions
       keymaps = {
-        ["g?"] = { "actions.show_help", mode = "n" },
+        ["g?"] = "actions.show_help",
         [""] = "actions.select",
-        [""] = { "actions.select", opts = { vertical = true } },
-        [""] = { "actions.select", opts = { horizontal = true } },
-        [""] = { "actions.select", opts = { tab = true } },
+        [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" },
+        [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" },
+        [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
         [""] = "actions.preview",
-        [""] = { "actions.close", mode = "n" },
+        [""] = "actions.close",
         [""] = "actions.refresh",
-        ["-"] = { "actions.parent", mode = "n" },
-        ["_"] = { "actions.open_cwd", mode = "n" },
-        ["`"] = { "actions.cd", mode = "n" },
-        ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
-        ["gs"] = { "actions.change_sort", mode = "n" },
+        ["-"] = "actions.parent",
+        ["_"] = "actions.open_cwd",
+        ["`"] = "actions.cd",
+        ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" },
+        ["gs"] = "actions.change_sort",
         ["gx"] = "actions.open_external",
-        ["g."] = { "actions.toggle_hidden", mode = "n" },
-        ["g\\"] = { "actions.toggle_trash", mode = "n" },
+        ["g."] = "actions.toggle_hidden",
+        ["g\\"] = "actions.toggle_trash",
       },
       -- Set to false to disable all of the above keymaps
       use_default_keymaps = true,
@@ -99,16 +97,15 @@ CONFIG                                                                *oil-confi
         show_hidden = false,
         -- This function defines what is considered a "hidden" file
         is_hidden_file = function(name, bufnr)
-          local m = name:match("^%.")
-          return m ~= nil
+          return vim.startswith(name, ".")
         end,
         -- This function defines what will never be shown, even when `show_hidden` is set
         is_always_hidden = function(name, bufnr)
           return false
         end,
-        -- Sort file names with numbers in a more intuitive order for humans.
-        -- Can be "fast", true, or false. "fast" will turn it off for large directories.
-        natural_order = "fast",
+        -- Sort file names in a more intuitive order for humans. Is less performant,
+        -- so you may want to set to false if you work with large directories.
+        natural_order = true,
         -- Sort file and directory names case insensitive
         case_insensitive = false,
         sort = {
@@ -117,15 +114,9 @@ CONFIG                                                                *oil-confi
           { "type", "asc" },
           { "name", "asc" },
         },
-        -- Customize the highlight group for the file name
-        highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
-          return nil
-        end,
       },
       -- Extra arguments to pass to SCP when moving/copying files over SSH
       extra_scp_args = {},
-      -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
-      extra_s3_args = {},
       -- EXPERIMENTAL support for performing file operations with git
       git = {
         -- Return true to automatically git add/mv/rm files
@@ -143,15 +134,12 @@ CONFIG                                                                *oil-confi
       float = {
         -- Padding around the floating window
         padding = 2,
-        -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
         max_width = 0,
         max_height = 0,
-        border = nil,
+        border = "rounded",
         win_options = {
           winblend = 0,
         },
-        -- optionally override the oil buffers window title with custom function: fun(winid: integer): string
-        get_win_title = nil,
         -- preview_split: Split direction: "auto", "left", "right", "above", "below".
         preview_split = "auto",
         -- This is the config that will be passed to nvim_open_win.
@@ -160,21 +148,8 @@ CONFIG                                                                *oil-confi
           return conf
         end,
       },
-      -- Configuration for the file preview window
-      preview_win = {
-        -- Whether the preview window is automatically updated when the cursor is moved
-        update_on_cursor_moved = true,
-        -- How to open the preview window "load"|"scratch"|"fast_scratch"
-        preview_method = "fast_scratch",
-        -- A function that returns true to disable preview on a file e.g. to avoid lag
-        disable_preview = function(filename)
-          return false
-        end,
-        -- Window-local options to use for preview window buffers
-        win_options = {},
-      },
-      -- Configuration for the floating action confirmation window
-      confirmation = {
+      -- Configuration for the actions floating preview window
+      preview = {
         -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
         -- min_width and max_width can be a single value or a list of mixed integer/float types.
         -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
@@ -191,10 +166,12 @@ CONFIG                                                                *oil-confi
         min_height = { 5, 0.1 },
         -- optionally define an integer/float for the exact height of the preview window
         height = nil,
-        border = nil,
+        border = "rounded",
         win_options = {
           winblend = 0,
         },
+        -- Whether the preview window is automatically updated when the cursor is moved
+        update_on_cursor_moved = true,
       },
       -- Configuration for the floating progress window
       progress = {
@@ -204,7 +181,7 @@ CONFIG                                                                *oil-confi
         max_height = { 10, 0.9 },
         min_height = { 5, 0.1 },
         height = nil,
-        border = nil,
+        border = "rounded",
         minimized_border = "none",
         win_options = {
           winblend = 0,
@@ -212,11 +189,11 @@ CONFIG                                                                *oil-confi
       },
       -- Configuration for the floating SSH window
       ssh = {
-        border = nil,
+        border = "rounded",
       },
       -- Configuration for the floating keymaps help window
       keymaps_help = {
-        border = nil,
+        border = "rounded",
       },
     })
 <
@@ -306,70 +283,40 @@ get_current_dir({bufnr}): nil|string                         *oil.get_current_di
     Parameters:
       {bufnr} `nil|integer`
 
-open_float({dir}, {opts}, {cb})                                   *oil.open_float*
+open_float({dir})                                                 *oil.open_float*
     Open oil browser in a floating window
 
     Parameters:
-      {dir}  `nil|string` When nil, open the parent of the current buffer, or
-             the cwd if current buffer is not a file
-      {opts} `nil|oil.OpenOpts`
-          {preview} `nil|oil.OpenPreviewOpts` When present, open the preview
-                    window after opening oil
-              {vertical}   `nil|boolean` Open the buffer in a vertical split
-              {horizontal} `nil|boolean` Open the buffer in a horizontal split
-              {split}      `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S
-                           plit modifier
-      {cb}   `nil|fun()` Called after the oil buffer is ready
+      {dir} `nil|string` When nil, open the parent of the current buffer, or the
+            cwd if current buffer is not a file
 
-toggle_float({dir}, {opts}, {cb})                               *oil.toggle_float*
+toggle_float({dir})                                             *oil.toggle_float*
     Open oil browser in a floating window, or close it if open
 
     Parameters:
-      {dir}  `nil|string` When nil, open the parent of the current buffer, or
-             the cwd if current buffer is not a file
-      {opts} `nil|oil.OpenOpts`
-          {preview} `nil|oil.OpenPreviewOpts` When present, open the preview
-                    window after opening oil
-              {vertical}   `nil|boolean` Open the buffer in a vertical split
-              {horizontal} `nil|boolean` Open the buffer in a horizontal split
-              {split}      `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S
-                           plit modifier
-      {cb}   `nil|fun()` Called after the oil buffer is ready
+      {dir} `nil|string` When nil, open the parent of the current buffer, or the
+            cwd if current buffer is not a file
 
-open({dir}, {opts}, {cb})                                               *oil.open*
+open({dir})                                                             *oil.open*
     Open oil browser for a directory
 
     Parameters:
-      {dir}  `nil|string` When nil, open the parent of the current buffer, or
-             the cwd if current buffer is not a file
-      {opts} `nil|oil.OpenOpts`
-          {preview} `nil|oil.OpenPreviewOpts` When present, open the preview
-                    window after opening oil
-              {vertical}   `nil|boolean` Open the buffer in a vertical split
-              {horizontal} `nil|boolean` Open the buffer in a horizontal split
-              {split}      `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S
-                           plit modifier
-      {cb}   `nil|fun()` Called after the oil buffer is ready
+      {dir} `nil|string` When nil, open the parent of the current buffer, or the
+            cwd if current buffer is not a file
 
-close({opts})                                                          *oil.close*
+close()                                                                *oil.close*
     Restore the buffer that was present when oil was opened
 
-    Parameters:
-      {opts} `nil|oil.CloseOpts`
-          {exit_if_last_buf} `nil|boolean` Exit vim if this oil buffer is the
-                             last open buffer
 
-open_preview({opts}, {callback})                                *oil.open_preview*
+open_preview({opts})                                            *oil.open_preview*
     Preview the entry under the cursor in a split
 
     Parameters:
-      {opts}     `nil|oil.OpenPreviewOpts`
-          {vertical}   `nil|boolean` Open the buffer in a vertical split
-          {horizontal} `nil|boolean` Open the buffer in a horizontal split
-          {split}      `nil|"aboveleft"|"belowright"|"topleft"|"botright"` Split
+      {opts} `nil|table`
+          {vertical}   `boolean` Open the buffer in a vertical split
+          {horizontal} `boolean` Open the buffer in a horizontal split
+          {split}      `"aboveleft"|"belowright"|"topleft"|"botright"` Split
                        modifier
-      {callback} `nil|fun(err: nil|string)` Called once the preview window has
-                 been opened
 
 select({opts}, {callback})                                            *oil.select*
     Select the entry under the cursor
@@ -383,10 +330,6 @@ select({opts}, {callback})                                            *oil.selec
           {tab}        `nil|boolean` Open the buffer in a new tab
           {close}      `nil|boolean` Close the original oil buffer once
                        selection is made
-          {handle_buffer_callback} `nil|fun(buf_id: integer)` If defined, all
-                       other buffer related options here would be ignored. This
-                       callback allows you to take over the process of opening
-                       the buffer yourself.
       {callback} `nil|fun(err: nil|string)` Called once all entries have been
                  opened
 
@@ -422,7 +365,6 @@ type                                                                 *column-typ
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
-      {align}     `"left"|"center"|"right"` Text alignment within the column
       {icons}     `table` Mapping of entry type to icon
 
 icon                                                                 *column-icon*
@@ -432,7 +374,6 @@ icon                                                                 *column-ico
     Parameters:
       {highlight}    `string|fun(value: string): string` Highlight group, or
                      function that returns a highlight group
-      {align}        `"left"|"center"|"right"` Text alignment within the column
       {default_file} `string` Fallback icon for files when nvim-web-devicons
                      returns nil
       {directory}    `string` Icon for directories
@@ -440,14 +381,13 @@ icon                                                                 *column-ico
                      the icon
 
 size                                                                 *column-size*
-    Adapters: files, ssh, s3
+    Adapters: files, ssh
     Sortable: this column can be used in view_props.sort
     The size of the file
 
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
-      {align}     `"left"|"center"|"right"` Text alignment within the column
 
 permissions                                                   *column-permissions*
     Adapters: files, ssh
@@ -457,7 +397,6 @@ permissions                                                   *column-permission
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
-      {align}     `"left"|"center"|"right"` Text alignment within the column
 
 ctime                                                               *column-ctime*
     Adapters: files
@@ -467,7 +406,6 @@ ctime                                                               *column-ctim
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
-      {align}     `"left"|"center"|"right"` Text alignment within the column
       {format}    `string` Format string (see :help strftime)
 
 mtime                                                               *column-mtime*
@@ -478,7 +416,6 @@ mtime                                                               *column-mtim
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
-      {align}     `"left"|"center"|"right"` Text alignment within the column
       {format}    `string` Format string (see :help strftime)
 
 atime                                                               *column-atime*
@@ -489,18 +426,16 @@ atime                                                               *column-atim
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
-      {align}     `"left"|"center"|"right"` Text alignment within the column
       {format}    `string` Format string (see :help strftime)
 
 birthtime                                                       *column-birthtime*
-    Adapters: files, s3
+    Adapters: files
     Sortable: this column can be used in view_props.sort
     The time the file was created
 
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
-      {align}     `"left"|"center"|"right"` Text alignment within the column
       {format}    `string` Format string (see :help strftime)
 
 --------------------------------------------------------------------------------
@@ -564,12 +499,6 @@ change_sort                                                  *actions.change_sor
 close                                                              *actions.close*
     Close oil and restore original buffer
 
-    Parameters:
-      {exit_if_last_buf} `boolean` Exit vim if oil is closed as the last buffer
-
-copy_to_system_clipboard                        *actions.copy_to_system_clipboard*
-    Copy the entry under the cursor to the system clipboard
-
 open_cmdline                                                *actions.open_cmdline*
     Open vim cmdline with current entry as an argument
 
@@ -590,31 +519,13 @@ open_terminal                                              *actions.open_termina
 parent                                                            *actions.parent*
     Navigate to the parent path
 
-paste_from_system_clipboard                  *actions.paste_from_system_clipboard*
-    Paste the system clipboard into the current oil directory
-
-    Parameters:
-      {delete_original} `boolean` Delete the original file after copying
-
 preview                                                          *actions.preview*
     Open the entry under the cursor in a preview window, or close the preview
     window if already open
 
-    Parameters:
-      {horizontal} `boolean` Open the buffer in a horizontal split
-      {split}      `"aboveleft"|"belowright"|"topleft"|"botright"` Split
-                   modifier
-      {vertical}   `boolean` Open the buffer in a vertical split
-
 preview_scroll_down                                  *actions.preview_scroll_down*
     Scroll down in the preview window
 
-preview_scroll_left                                  *actions.preview_scroll_left*
-    Scroll left in the preview window
-
-preview_scroll_right                                *actions.preview_scroll_right*
-    Scroll right in the preview window
-
 preview_scroll_up                                      *actions.preview_scroll_up*
     Scroll up in the preview window
 
@@ -644,9 +555,6 @@ send_to_qflist                                            *actions.send_to_qflis
     Parameters:
       {action} `"r"|"a"` Replace or add to current quickfix list (see
                |setqflist-action|)
-      {only_matching_search} `boolean` Whether to only add the files that
-               matches the last search. This option only applies when search
-               highlighting is active
       {target} `"qflist"|"loclist"` The target list to send files to
 
 show_help                                                      *actions.show_help*
@@ -668,57 +576,24 @@ yank_entry                                                    *actions.yank_entr
 --------------------------------------------------------------------------------
 HIGHLIGHTS                                                        *oil-highlights*
 
-OilEmpty                                                             *hl-OilEmpty*
-    Empty column values
-
-OilHidden                                                           *hl-OilHidden*
-    Hidden entry in an oil buffer
-
 OilDir                                                                 *hl-OilDir*
     Directory names in an oil buffer
 
-OilDirHidden                                                     *hl-OilDirHidden*
-    Hidden directory names in an oil buffer
-
 OilDirIcon                                                         *hl-OilDirIcon*
     Icon for directories
 
 OilSocket                                                           *hl-OilSocket*
     Socket files in an oil buffer
 
-OilSocketHidden                                               *hl-OilSocketHidden*
-    Hidden socket files in an oil buffer
-
 OilLink                                                               *hl-OilLink*
     Soft links in an oil buffer
 
-OilOrphanLink                                                   *hl-OilOrphanLink*
-    Orphaned soft links in an oil buffer
-
-OilLinkHidden                                                   *hl-OilLinkHidden*
-    Hidden soft links in an oil buffer
-
-OilOrphanLinkHidden                                       *hl-OilOrphanLinkHidden*
-    Hidden orphaned soft links in an oil buffer
-
 OilLinkTarget                                                   *hl-OilLinkTarget*
     The target of a soft link
 
-OilOrphanLinkTarget                                       *hl-OilOrphanLinkTarget*
-    The target of an orphaned soft link
-
-OilLinkTargetHidden                                       *hl-OilLinkTargetHidden*
-    The target of a hidden soft link
-
-OilOrphanLinkTargetHidden                           *hl-OilOrphanLinkTargetHidden*
-    The target of an hidden orphaned soft link
-
 OilFile                                                               *hl-OilFile*
     Normal files in an oil buffer
 
-OilFileHidden                                                   *hl-OilFileHidden*
-    Hidden normal files in an oil buffer
-
 OilCreate                                                           *hl-OilCreate*
     Create action in the oil preview window
 
@@ -757,13 +632,13 @@ of being permanently deleted. You can browse the trash for a directory using
 the `toggle_trash` action (bound to `g\` by default). You can view all files
 in the trash with `:Oil --trash /`.
 
-To restore files, simply move them from the trash to the desired destination,
-the same as any other file operation. If you delete files from the trash they
-will be permanently deleted (purged).
+To restore files, simply delete them from the trash and put them in the desired
+destination, the same as any other file operation. If you delete files from the
+trash they will be permanently deleted (purged).
 
 Linux:
     Oil supports the FreeDesktop trash specification.
-    https://specifications.freedesktop.org/trash-spec/1.0/
+    https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html
     All features should work.
 
 Mac:
@@ -772,7 +647,7 @@ Mac:
     (instead of being able to see files that were trashed from a directory).
 
 Windows:
-    Oil supports the Windows Recycle Bin. All features should work.
+    Oil does not yet support the Windows trash. PRs are welcome!
 
 ================================================================================
 vim:tw=80:ts=2:ft=help:norl:syntax=help:
diff --git a/doc/recipes.md b/doc/recipes.md
index 0a19598..b2b875b 100644
--- a/doc/recipes.md
+++ b/doc/recipes.md
@@ -5,7 +5,6 @@ Have a cool recipe to share? Open a pull request and add it to this doc!
 
 
 - [Toggle file detail view](#toggle-file-detail-view)
-- [Show CWD in the winbar](#show-cwd-in-the-winbar)
 - [Hide gitignored files and show git tracked hidden files](#hide-gitignored-files-and-show-git-tracked-hidden-files)
 
 
@@ -31,28 +30,6 @@ require("oil").setup({
 })
 ```
 
-## Show CWD in the winbar
-
-```lua
--- Declare a global function to retrieve the current directory
-function _G.get_oil_winbar()
-  local bufnr = vim.api.nvim_win_get_buf(vim.g.statusline_winid)
-  local dir = require("oil").get_current_dir(bufnr)
-  if dir then
-    return vim.fn.fnamemodify(dir, ":~")
-  else
-    -- If there is no current directory (e.g. over ssh), just show the buffer name
-    return vim.api.nvim_buf_get_name(0)
-  end
-end
-
-require("oil").setup({
-  win_options = {
-    winbar = "%!v:lua.get_oil_winbar()",
-  },
-})
-```
-
 ## Hide gitignored files and show git tracked hidden files
 
 ```lua
@@ -111,9 +88,7 @@ require("oil").setup({
       local dir = require("oil").get_current_dir(bufnr)
       local is_dotfile = vim.startswith(name, ".") and name ~= ".."
       -- if no local directory (e.g. for ssh connections), just hide dotfiles
-      if not dir then
-        return is_dotfile
-      end
+      if not dir then return is_dotfile end
       -- dotfiles are considered hidden unless tracked
       if is_dotfile then
         return not git_status[dir].tracked[name]
diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua
index 1c61999..6a2a5ff 100644
--- a/lua/oil/actions.lua
+++ b/lua/oil/actions.lua
@@ -69,21 +69,7 @@ M.select_tab = {
 
 M.preview = {
   desc = "Open the entry under the cursor in a preview window, or close the preview window if already open",
-  parameters = {
-    vertical = {
-      type = "boolean",
-      desc = "Open the buffer in a vertical split",
-    },
-    horizontal = {
-      type = "boolean",
-      desc = "Open the buffer in a horizontal split",
-    },
-    split = {
-      type = '"aboveleft"|"belowright"|"topleft"|"botright"',
-      desc = "Split modifier",
-    },
-  },
-  callback = function(opts)
+  callback = function()
     local entry = oil.get_cursor_entry()
     if not entry then
       vim.notify("Could not find entry under cursor", vim.log.levels.ERROR)
@@ -102,7 +88,7 @@ M.preview = {
         return
       end
     end
-    oil.open_preview(opts)
+    oil.open_preview()
   end,
 }
 
@@ -136,30 +122,6 @@ M.preview_scroll_up = {
   end,
 }
 
-M.preview_scroll_left = {
-  desc = "Scroll left in the preview window",
-  callback = function()
-    local winid = util.get_preview_win()
-    if winid then
-      vim.api.nvim_win_call(winid, function()
-        vim.cmd.normal({ "zH", bang = true })
-      end)
-    end
-  end,
-}
-
-M.preview_scroll_right = {
-  desc = "Scroll right in the preview window",
-  callback = function()
-    local winid = util.get_preview_win()
-    if winid then
-      vim.api.nvim_win_call(winid, function()
-        vim.cmd.normal({ "zL", bang = true })
-      end)
-    end
-  end,
-}
-
 M.parent = {
   desc = "Navigate to the parent path",
   callback = oil.open,
@@ -167,16 +129,7 @@ M.parent = {
 
 M.close = {
   desc = "Close oil and restore original buffer",
-  callback = function(opts)
-    opts = opts or {}
-    oil.close(opts)
-  end,
-  parameters = {
-    exit_if_last_buf = {
-      type = "boolean",
-      desc = "Exit vim if oil is closed as the last buffer",
-    },
-  },
+  callback = oil.close,
 }
 
 ---@param cmd string
@@ -253,24 +206,13 @@ M.open_terminal = {
       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)
-      if vim.fn.has("nvim-0.11") == 1 then
-        vim.fn.jobstart(vim.o.shell, { cwd = dir, term = true })
-      else
-        ---@diagnostic disable-next-line: deprecated
-        vim.fn.termopen(vim.o.shell, { cwd = dir })
-      end
+      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
-      if vim.fn.has("nvim-0.11") == 1 then
-        term_id = vim.fn.jobstart(cmd, { term = true })
-      else
-        ---@diagnostic disable-next-line: deprecated
-        term_id = vim.fn.termopen(cmd)
-      end
+      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
@@ -410,11 +352,7 @@ M.yank_entry = {
     if not entry or not dir then
       return
     end
-    local name = entry.name
-    if entry.type == "directory" then
-      name = name .. "/"
-    end
-    local path = dir .. name
+    local path = dir .. entry.name
     if opts.modify then
       path = vim.fn.fnamemodify(path, opts.modify)
     end
@@ -453,26 +391,6 @@ M.copy_entry_filename = {
   end,
 }
 
-M.copy_to_system_clipboard = {
-  desc = "Copy the entry under the cursor to the system clipboard",
-  callback = function()
-    require("oil.clipboard").copy_to_system_clipboard()
-  end,
-}
-
-M.paste_from_system_clipboard = {
-  desc = "Paste the system clipboard into the current oil directory",
-  callback = function(opts)
-    require("oil.clipboard").paste_from_system_clipboard(opts and opts.delete_original)
-  end,
-  parameters = {
-    delete_original = {
-      type = "boolean",
-      desc = "Delete the original file after copying",
-    },
-  },
-}
-
 M.open_cmdline_dir = {
   desc = "Open vim cmdline with current directory as an argument",
   deprecated = true,
@@ -559,12 +477,10 @@ M.send_to_qflist = {
     opts = vim.tbl_deep_extend("keep", opts or {}, {
       target = "qflist",
       action = "r",
-      only_matching_search = false,
     })
     util.send_to_quickfix({
       target = opts.target,
       action = opts.action,
-      only_matching_search = opts.only_matching_search,
     })
   end,
   parameters = {
@@ -576,10 +492,6 @@ M.send_to_qflist = {
       type = '"r"|"a"',
       desc = "Replace or add to current quickfix list (see |setqflist-action|)",
     },
-    only_matching_search = {
-      type = "boolean",
-      desc = "Whether to only add the files that matches the last search. This option only applies when search highlighting is active",
-    },
   },
 }
 
diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 9785c3a..f65cbf7 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -4,15 +4,14 @@ local config = require("oil.config")
 local constants = require("oil.constants")
 local fs = require("oil.fs")
 local git = require("oil.git")
-local log = require("oil.log")
 local permissions = require("oil.adapters.files.permissions")
+local trash = require("oil.adapters.files.trash")
 local util = require("oil.util")
 local uv = vim.uv or vim.loop
 
 local M = {}
 
 local FIELD_NAME = constants.FIELD_NAME
-local FIELD_TYPE = constants.FIELD_TYPE
 local FIELD_META = constants.FIELD_META
 
 local function read_link_data(path, cb)
@@ -35,9 +34,6 @@ local function read_link_data(path, cb)
   )
 end
 
----@class (exact) oil.FilesAdapter: oil.Adapter
----@field to_short_os_path fun(path: string, entry_type: nil|oil.EntryType): string
-
 ---@param path string
 ---@param entry_type nil|oil.EntryType
 ---@return string
@@ -51,12 +47,21 @@ end
 
 local file_columns = {}
 
+local fs_stat_meta_fields = {
+  stat = function(parent_url, entry, cb)
+    local _, path = util.parse_url(parent_url)
+    assert(path)
+    local dir = fs.posix_to_os_path(path .. entry[FIELD_NAME])
+    uv.fs_stat(dir, cb)
+  end,
+}
+
 file_columns.size = {
-  require_stat = true,
+  meta_fields = fs_stat_meta_fields,
 
   render = function(entry, conf)
     local meta = entry[FIELD_META]
-    local stat = meta and meta.stat
+    local stat = meta.stat
     if not stat then
       return columns.EMPTY
     end
@@ -73,7 +78,7 @@ file_columns.size = {
 
   get_sort_value = function(entry)
     local meta = entry[FIELD_META]
-    local stat = meta and meta.stat
+    local stat = meta.stat
     if stat then
       return stat.size
     else
@@ -89,11 +94,11 @@ file_columns.size = {
 -- TODO support file permissions on windows
 if not fs.is_windows then
   file_columns.permissions = {
-    require_stat = true,
+    meta_fields = fs_stat_meta_fields,
 
     render = function(entry, conf)
       local meta = entry[FIELD_META]
-      local stat = meta and meta.stat
+      local stat = meta.stat
       if not stat then
         return columns.EMPTY
       end
@@ -106,7 +111,7 @@ if not fs.is_windows then
 
     compare = function(entry, parsed_value)
       local meta = entry[FIELD_META]
-      if parsed_value and meta and meta.stat and meta.stat.mode then
+      if parsed_value and meta.stat and meta.stat.mode then
         local mask = bit.lshift(1, 12) - 1
         local old_mode = bit.band(meta.stat.mode, mask)
         if parsed_value ~= old_mode then
@@ -152,11 +157,11 @@ end)
 
 for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
   file_columns[time_key] = {
-    require_stat = true,
+    meta_fields = fs_stat_meta_fields,
 
     render = function(entry, conf)
       local meta = entry[FIELD_META]
-      local stat = meta and meta.stat
+      local stat = meta.stat
       if not stat then
         return columns.EMPTY
       end
@@ -179,20 +184,7 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
       local fmt = conf and conf.format
       local pattern
       if fmt then
-        -- Replace placeholders with a pattern that matches non-space characters (e.g. %H -> %S+)
-        -- and whitespace with a pattern that matches any amount of whitespace
-        -- e.g. "%b %d %Y" -> "%S+%s+%S+%s+%S+"
-        pattern = fmt
-          :gsub("%%.", "%%S+")
-          :gsub("%s+", "%%s+")
-          -- escape `()[]` because those are special characters in Lua patterns
-          :gsub(
-            "%(",
-            "%%("
-          )
-          :gsub("%)", "%%)")
-          :gsub("%[", "%%[")
-          :gsub("%]", "%%]")
+        pattern = fmt:gsub("%%.", "%%S+")
       else
         pattern = "%S+%s+%d+%s+%d%d:?%d%d"
       end
@@ -201,7 +193,7 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
 
     get_sort_value = function(entry)
       local meta = entry[FIELD_META]
-      local stat = meta and meta.stat
+      local stat = meta.stat
       if stat then
         return stat[time_key].sec
       else
@@ -211,20 +203,6 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
   }
 end
 
----@param column_defs table[]
----@return boolean
-local function columns_require_stat(column_defs)
-  for _, def in ipairs(column_defs) do
-    local name = util.split_config(def)
-    local column = M.get_column(name)
-    ---@diagnostic disable-next-line: undefined-field We only put this on the files adapter columns
-    if column and column.require_stat then
-      return true
-    end
-  end
-  return false
-end
-
 ---@param name string
 ---@return nil|oil.ColumnDefinition
 M.get_column = function(name)
@@ -276,7 +254,7 @@ M.normalize_url = function(url, callback)
           local norm_path = util.addslash(fs.os_to_posix_path(realpath))
           callback(scheme .. norm_path)
         else
-          callback(vim.fn.fnamemodify(realpath, ":."))
+          callback(realpath)
         end
       end)
     )
@@ -302,102 +280,11 @@ M.get_entry_path = function(url, entry, cb)
   end
 end
 
----@param parent_dir string
----@param entry oil.InternalEntry
----@param require_stat boolean
----@param cb fun(err?: string)
-local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
-  local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME])
-  local meta = entry[FIELD_META]
-  if not meta then
-    meta = {}
-    entry[FIELD_META] = meta
-  end
-
-  -- Sometimes fs_readdir entries don't have a type, so we need to stat them.
-  -- See https://github.com/stevearc/oil.nvim/issues/543
-  if not require_stat and not entry[FIELD_TYPE] then
-    require_stat = true
-  end
-
-  -- Make sure we always get fs_stat info for links
-  if entry[FIELD_TYPE] == "link" then
-    read_link_data(entry_path, function(link_err, link, link_stat)
-      if link_err then
-        log.warn("Error reading link data %s: %s", entry_path, link_err)
-        return cb()
-      end
-      meta.link = link
-      if link_stat then
-        -- Use the fstat of the linked file as the stat for the link
-        meta.link_stat = link_stat
-        meta.stat = link_stat
-      elseif require_stat then
-        -- The link is broken, so let's use the stat of the link itself
-        uv.fs_lstat(entry_path, function(stat_err, stat)
-          if stat_err then
-            log.warn("Error lstat link file %s: %s", entry_path, stat_err)
-            return cb()
-          end
-          meta.stat = stat
-          cb()
-        end)
-        return
-      end
-
-      cb()
-    end)
-  elseif require_stat then
-    uv.fs_stat(entry_path, function(stat_err, stat)
-      if stat_err then
-        log.warn("Error stat file %s: %s", entry_path, stat_err)
-        return cb()
-      end
-      assert(stat)
-      entry[FIELD_TYPE] = stat.type
-      meta.stat = stat
-      cb()
-    end)
-  else
-    cb()
-  end
-end
-
--- On windows, sometimes the entry type from fs_readdir is "link" but the actual type is not.
--- See https://github.com/stevearc/oil.nvim/issues/535
-if fs.is_windows then
-  local old_fetch_metadata = fetch_entry_metadata
-  fetch_entry_metadata = function(parent_dir, entry, require_stat, cb)
-    if entry[FIELD_TYPE] == "link" then
-      local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME])
-      uv.fs_lstat(entry_path, function(stat_err, stat)
-        if stat_err then
-          log.warn("Error lstat link file %s: %s", entry_path, stat_err)
-          return old_fetch_metadata(parent_dir, entry, require_stat, cb)
-        end
-        assert(stat)
-        entry[FIELD_TYPE] = stat.type
-        local meta = entry[FIELD_META]
-        if not meta then
-          meta = {}
-          entry[FIELD_META] = meta
-        end
-        meta.stat = stat
-        old_fetch_metadata(parent_dir, entry, require_stat, cb)
-      end)
-    else
-      return old_fetch_metadata(parent_dir, entry, require_stat, cb)
-    end
-  end
-end
-
 ---@param url string
 ---@param column_defs string[]
 ---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
 local function list_windows_drives(url, column_defs, cb)
-  local _, path = util.parse_url(url)
-  assert(path)
-  local require_stat = columns_require_stat(column_defs)
+  local fetch_meta = columns.get_metadata_fetcher(M, column_defs)
   local stdout = ""
   local jid = vim.fn.jobstart({ "wmic", "logicaldisk", "get", "name" }, {
     stdout_buffered = true,
@@ -427,8 +314,14 @@ local function list_windows_drives(url, column_defs, cb)
         else
           disk = disk:gsub(":%s*$", "")
           local cache_entry = cache.create_entry(url, disk, "directory")
-          table.insert(internal_entries, cache_entry)
-          fetch_entry_metadata(path, cache_entry, require_stat, complete_disk_cb)
+          fetch_meta(url, cache_entry, function(err)
+            if err then
+              complete_disk_cb(err)
+            else
+              table.insert(internal_entries, cache_entry)
+              complete_disk_cb()
+            end
+          end)
         end
       end
     end,
@@ -448,7 +341,7 @@ M.list = function(url, column_defs, cb)
     return list_windows_drives(url, column_defs, cb)
   end
   local dir = fs.posix_to_os_path(path)
-  local require_stat = columns_require_stat(column_defs)
+  local fetch_meta = columns.get_metadata_fetcher(M, column_defs)
 
   ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
   uv.fs_opendir(dir, function(open_err, fd)
@@ -480,8 +373,28 @@ M.list = function(url, column_defs, cb)
           end)
           for _, entry in ipairs(entries) do
             local cache_entry = cache.create_entry(url, entry.name, entry.type)
-            table.insert(internal_entries, cache_entry)
-            fetch_entry_metadata(path, cache_entry, require_stat, poll)
+            fetch_meta(url, cache_entry, function(meta_err)
+              table.insert(internal_entries, cache_entry)
+              local meta = cache_entry[FIELD_META]
+              -- Make sure we always get fs_stat info for links
+              if entry.type == "link" then
+                read_link_data(fs.join(dir, entry.name), function(link_err, link, link_stat)
+                  if link_err then
+                    poll(link_err)
+                  else
+                    if not meta then
+                      meta = {}
+                      cache_entry[FIELD_META] = meta
+                    end
+                    meta.link = link
+                    meta.link_stat = link_stat
+                    poll()
+                  end
+                end)
+              else
+                poll()
+              end
+            end)
           end
         else
           uv.fs_closedir(fd, function(close_err)
@@ -499,6 +412,26 @@ M.list = function(url, column_defs, cb)
   end, 10000)
 end
 
+---@type nil|integer[]
+local _group_ids
+---@return integer[]
+local function get_group_ids()
+  if not _group_ids then
+    local output = vim.fn.system({ "id", "-G" })
+    if vim.v.shell_error == 0 then
+      _group_ids = vim.tbl_map(tonumber, vim.split(output, "%s+", { trimempty = true }))
+    else
+      -- If the id command fails, fall back to just using the process group
+      _group_ids = { uv.getgid() }
+      vim.notify(
+        "[oil] missing the `id` command. Some directories may not be modifiable even if you have group access.",
+        vim.log.levels.WARN
+      )
+    end
+  end
+  return _group_ids
+end
+
 ---@param bufnr integer
 ---@return boolean
 M.is_modifiable = function(bufnr)
@@ -514,8 +447,20 @@ M.is_modifiable = function(bufnr)
     return true
   end
 
-  -- fs_access can return nil, force boolean return
-  return uv.fs_access(dir, "W") == true
+  -- Can't do permissions checks on windows
+  if fs.is_windows then
+    return true
+  end
+
+  local uid = uv.getuid()
+  local rwx = stat.mode
+  if uid == stat.uid then
+    rwx = bit.bor(rwx, bit.rshift(stat.mode, 6))
+  end
+  if vim.tbl_contains(get_group_ids(), stat.gid) then
+    rwx = bit.bor(rwx, bit.rshift(stat.mode, 3))
+  end
+  return bit.band(rwx, 2) ~= 0
 end
 
 ---@param action oil.Action
@@ -539,7 +484,7 @@ M.render_action = function(action)
       return string.format("DELETE %s", short_path)
     end
   elseif action.type == "move" or action.type == "copy" then
-    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
+    local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
     if dest_adapter == M then
       local _, src_path = util.parse_url(action.src_url)
       assert(src_path)
@@ -619,12 +564,20 @@ M.perform_action = function(action, cb)
     end
 
     if config.delete_to_trash then
-      require("oil.adapters.trash").delete_to_trash(path, cb)
+      if config.trash_command then
+        vim.notify_once(
+          "Oil now has native support for trash. Remove the `trash_command` from your config to try it out!",
+          vim.log.levels.WARN
+        )
+        trash.recursive_delete(path, cb)
+      else
+        require("oil.adapters.trash").delete_to_trash(path, cb)
+      end
     else
       fs.recursive_delete(action.entry_type, path, cb)
     end
   elseif action.type == "move" then
-    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
+    local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
     if dest_adapter == M then
       local _, src_path = util.parse_url(action.src_url)
       assert(src_path)
@@ -642,7 +595,7 @@ M.perform_action = function(action, cb)
       cb("files adapter doesn't support cross-adapter move")
     end
   elseif action.type == "copy" then
-    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
+    local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
     if dest_adapter == M then
       local _, src_path = util.parse_url(action.src_url)
       assert(src_path)
diff --git a/lua/oil/adapters/files/permissions.lua b/lua/oil/adapters/files/permissions.lua
index 6c306a6..cf50b55 100644
--- a/lua/oil/adapters/files/permissions.lua
+++ b/lua/oil/adapters/files/permissions.lua
@@ -1,6 +1,6 @@
 local M = {}
 
----@param exe_modifier false|string
+---@param exe_modifier nil|false|string
 ---@param num integer
 ---@return string
 local function perm_to_str(exe_modifier, num)
diff --git a/lua/oil/adapters/files/trash.lua b/lua/oil/adapters/files/trash.lua
new file mode 100644
index 0000000..5236f5b
--- /dev/null
+++ b/lua/oil/adapters/files/trash.lua
@@ -0,0 +1,44 @@
+local config = require("oil.config")
+local M = {}
+
+M.recursive_delete = function(path, cb)
+  local stdout = {}
+  local stderr = {}
+  local cmd
+  if config.trash_command:find("%s") then
+    cmd = string.format("%s %s", config.trash_command, vim.fn.shellescape(path))
+  else
+    cmd = { config.trash_command, path }
+  end
+  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 = function(j, exit_code)
+      if exit_code == 0 then
+        cb()
+      else
+        cb(
+          string.format(
+            "Error moving '%s' to trash:\n  stdout: %s\n  stderr: %s",
+            path,
+            table.concat(stdout, "\n  "),
+            table.concat(stderr, "\n  ")
+          )
+        )
+      end
+    end,
+  })
+  if jid == 0 then
+    cb(string.format("Passed invalid argument '%s' to '%s'", path, config.trash_command))
+  elseif jid == -1 then
+    cb(string.format("'%s' is not executable", config.trash_command))
+  end
+end
+
+return M
diff --git a/lua/oil/adapters/s3.lua b/lua/oil/adapters/s3.lua
deleted file mode 100644
index b7b5400..0000000
--- a/lua/oil/adapters/s3.lua
+++ /dev/null
@@ -1,389 +0,0 @@
-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 s3fs = require("oil.adapters.s3.s3fs")
-local util = require("oil.util")
-local M = {}
-
-local FIELD_META = constants.FIELD_META
-
----@class (exact) oil.s3Url
----@field scheme string
----@field bucket nil|string
----@field path nil|string
-
----@param oil_url string
----@return oil.s3Url
-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 bucket, path = url:match("^([^/]+)/?(.*)$")
-  ret.bucket = bucket
-  ret.path = path ~= "" and path or nil
-  if not ret.bucket and ret.path then
-    error(string.format("Parsing error for s3 url: %s", oil_url))
-  end
-  ---@cast ret oil.s3Url
-  return ret
-end
-
----@param url oil.s3Url
----@return string
-local function url_to_str(url)
-  local pieces = { url.scheme }
-  if url.bucket then
-    assert(url.bucket ~= "")
-    table.insert(pieces, url.bucket)
-    table.insert(pieces, "/")
-  end
-  if url.path then
-    assert(url.path ~= "")
-    table.insert(pieces, url.path)
-  end
-  return table.concat(pieces, "")
-end
-
----@param url oil.s3Url
----@param is_folder boolean
----@return string
-local function url_to_s3(url, is_folder)
-  local pieces = { "s3://" }
-  if url.bucket then
-    assert(url.bucket ~= "")
-    table.insert(pieces, url.bucket)
-    table.insert(pieces, "/")
-  end
-  if url.path then
-    assert(url.path ~= "")
-    table.insert(pieces, url.path)
-    if is_folder and not vim.endswith(url.path, "/") then
-      table.insert(pieces, "/")
-    end
-  end
-  return table.concat(pieces, "")
-end
-
----@param url oil.s3Url
----@return boolean
-local function is_bucket(url)
-  assert(url.bucket and url.bucket ~= "")
-  if url.path then
-    assert(url.path ~= "")
-    return false
-  end
-  return true
-end
-
-local s3_columns = {}
-s3_columns.size = {
-  render = function(entry, conf)
-    local meta = entry[FIELD_META]
-    if not meta or not meta.size then
-      return ""
-    elseif 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,
-}
-
-s3_columns.birthtime = {
-  render = function(entry, conf)
-    local meta = entry[FIELD_META]
-    if not meta or not meta.date then
-      return ""
-    else
-      return meta.date
-    end
-  end,
-
-  parse = function(line, conf)
-    return line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$")
-  end,
-
-  get_sort_value = function(entry)
-    local meta = entry[FIELD_META]
-    if meta and meta.date then
-      local year, month, day, hour, min, sec =
-        meta.date:match("^(%d+)%-(%d+)%-(%d+)%s(%d+):(%d+):(%d+)$")
-      local time =
-        os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
-      return time
-    else
-      return 0
-    end
-  end,
-}
-
----@param name string
----@return nil|oil.ColumnDefinition
-M.get_column = function(name)
-  return s3_columns[name]
-end
-
----@param bufname string
----@return string
-M.get_parent = function(bufname)
-  local res = M.parse_url(bufname)
-  if res.path then
-    assert(res.path ~= "")
-    local path = pathutil.parent(res.path)
-    res.path = path ~= "" and path or nil
-  else
-    res.bucket = nil
-  end
-  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("aws") ~= 1 then
-    callback("`aws` is not executable. Can you run `aws s3 ls`?")
-    return
-  end
-
-  local res = M.parse_url(url)
-  s3fs.list_dir(url, url_to_s3(res, true), callback)
-end
-
----@param bufnr integer
----@return boolean
-M.is_modifiable = function(bufnr)
-  -- default assumption is that everything is modifiable
-  return true
-end
-
----@param action oil.Action
----@return string
-M.render_action = function(action)
-  local is_folder = action.entry_type == "directory"
-  if action.type == "create" then
-    local res = M.parse_url(action.url)
-    local extra = is_bucket(res) and "BUCKET " or ""
-    return string.format("CREATE %s%s", extra, url_to_s3(res, is_folder))
-  elseif action.type == "delete" then
-    local res = M.parse_url(action.url)
-    local extra = is_bucket(res) and "BUCKET " or ""
-    return string.format("DELETE %s%s", extra, url_to_s3(res, is_folder))
-  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)
-      dest = url_to_s3(M.parse_url(dest), is_folder)
-    elseif 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)
-      src = url_to_s3(M.parse_url(src), is_folder)
-    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 action oil.Action
----@param cb fun(err: nil|string)
-M.perform_action = function(action, cb)
-  local is_folder = action.entry_type == "directory"
-  if action.type == "create" then
-    local res = M.parse_url(action.url)
-    local bucket = is_bucket(res)
-
-    if action.entry_type == "directory" and bucket then
-      s3fs.mb(url_to_s3(res, true), cb)
-    elseif action.entry_type == "directory" or action.entry_type == "file" then
-      s3fs.touch(url_to_s3(res, is_folder), cb)
-    else
-      cb(string.format("Bad entry type on s3 create action: %s", action.entry_type))
-    end
-  elseif action.type == "delete" then
-    local res = M.parse_url(action.url)
-    local bucket = is_bucket(res)
-
-    if action.entry_type == "directory" and bucket then
-      s3fs.rb(url_to_s3(res, true), cb)
-    elseif action.entry_type == "directory" or action.entry_type == "file" then
-      s3fs.rm(url_to_s3(res, is_folder), is_folder, cb)
-    else
-      cb(string.format("Bad entry type on s3 delete action: %s", action.entry_type))
-    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 src_adapter ~= files) or (dest_adapter ~= M and dest_adapter ~= files)
-    then
-      cb(
-        string.format(
-          "We should never attempt to move from the %s adapter to the %s adapter.",
-          src_adapter.name,
-          dest_adapter.name
-        )
-      )
-    end
-
-    local src, _
-    if src_adapter == M then
-      local src_res = M.parse_url(action.src_url)
-      src = url_to_s3(src_res, is_folder)
-    else
-      _, src = util.parse_url(action.src_url)
-    end
-    assert(src)
-
-    local dest
-    if dest_adapter == M then
-      local dest_res = M.parse_url(action.dest_url)
-      dest = url_to_s3(dest_res, is_folder)
-    else
-      _, dest = util.parse_url(action.dest_url)
-    end
-    assert(dest)
-
-    s3fs.mv(src, dest, is_folder, cb)
-  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 src_adapter ~= files) or (dest_adapter ~= M and dest_adapter ~= files)
-    then
-      cb(
-        string.format(
-          "We should never attempt to copy from the %s adapter to the %s adapter.",
-          src_adapter.name,
-          dest_adapter.name
-        )
-      )
-    end
-
-    local src, _
-    if src_adapter == M then
-      local src_res = M.parse_url(action.src_url)
-      src = url_to_s3(src_res, is_folder)
-    else
-      _, src = util.parse_url(action.src_url)
-    end
-    assert(src)
-
-    local dest
-    if dest_adapter == M then
-      local dest_res = M.parse_url(action.dest_url)
-      dest = url_to_s3(dest_res, is_folder)
-    else
-      _, dest = util.parse_url(action.dest_url)
-    end
-    assert(dest)
-
-    s3fs.cp(src, dest, is_folder, cb)
-  else
-    cb(string.format("Bad action type: %s", action.type))
-  end
-end
-
-M.supported_cross_adapter_actions = { files = "move" }
-
----@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, "s3_XXXXXX"))
-  if fd then
-    vim.loop.fs_close(fd)
-  end
-  local tmp_bufnr = vim.fn.bufadd(tmpfile)
-
-  s3fs.cp(url_to_s3(url, false), tmpfile, false, 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, "s3_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)
-
-  s3fs.cp(tmpfile, url_to_s3(url, false), false, 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/adapters/s3/s3fs.lua b/lua/oil/adapters/s3/s3fs.lua
deleted file mode 100644
index 2e16307..0000000
--- a/lua/oil/adapters/s3/s3fs.lua
+++ /dev/null
@@ -1,149 +0,0 @@
-local cache = require("oil.cache")
-local config = require("oil.config")
-local constants = require("oil.constants")
-local shell = require("oil.shell")
-local util = require("oil.util")
-
-local M = {}
-
-local FIELD_META = constants.FIELD_META
-
----@param line string
----@return string Name of entry
----@return oil.EntryType
----@return table Metadata for entry
-local function parse_ls_line_bucket(line)
-  local date, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$")
-  if not date or not name then
-    error(string.format("Could not parse '%s'", line))
-  end
-  local type = "directory"
-  local meta = { date = date }
-  return name, type, meta
-end
-
----@param line string
----@return string Name of entry
----@return oil.EntryType
----@return table Metadata for entry
-local function parse_ls_line_file(line)
-  local name = line:match("^%s+PRE%s+(.*)/$")
-  local type = "directory"
-  local meta = {}
-  if name then
-    return name, type, meta
-  end
-  local date, size
-  date, size, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(%d+)%s+(.*)$")
-  if not name then
-    error(string.format("Could not parse '%s'", line))
-  end
-  type = "file"
-  meta = { date = date, size = tonumber(size) }
-  return name, type, meta
-end
-
----@param cmd string[] cmd and flags
----@return string[] Shell command to run
-local function create_s3_command(cmd)
-  local full_cmd = vim.list_extend({ "aws", "s3" }, cmd)
-  return vim.list_extend(full_cmd, config.extra_s3_args)
-end
-
----@param url string
----@param path string
----@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
-function M.list_dir(url, path, callback)
-  local cmd = create_s3_command({ "ls", path, "--color=off", "--no-cli-pager" })
-  shell.run(cmd, function(err, lines)
-    if err then
-      return callback(err)
-    end
-    assert(lines)
-    local cache_entries = {}
-    local url_path, _
-    _, url_path = util.parse_url(url)
-    local is_top_level = url_path == nil or url_path:match("/") == nil
-    local parse_ls_line = is_top_level and parse_ls_line_bucket or parse_ls_line_file
-    for _, line in ipairs(lines) do
-      if line ~= "" then
-        local name, type, meta = parse_ls_line(line)
-        -- in s3 '-' can be used to create an "empty folder"
-        if name ~= "-" then
-          local cache_entry = cache.create_entry(url, name, type)
-          table.insert(cache_entries, cache_entry)
-          cache_entry[FIELD_META] = meta
-        end
-      end
-    end
-    callback(nil, cache_entries)
-  end)
-end
-
---- Create files
----@param path string
----@param callback fun(err: nil|string)
-function M.touch(path, callback)
-  -- here "-" means that we copy from stdin
-  local cmd = create_s3_command({ "cp", "-", path })
-  shell.run(cmd, { stdin = "null" }, callback)
-end
-
---- Remove files
----@param path string
----@param is_folder boolean
----@param callback fun(err: nil|string)
-function M.rm(path, is_folder, callback)
-  local main_cmd = { "rm", path }
-  if is_folder then
-    table.insert(main_cmd, "--recursive")
-  end
-  local cmd = create_s3_command(main_cmd)
-  shell.run(cmd, callback)
-end
-
---- Remove bucket
----@param bucket string
----@param callback fun(err: nil|string)
-function M.rb(bucket, callback)
-  local cmd = create_s3_command({ "rb", bucket })
-  shell.run(cmd, callback)
-end
-
---- Make bucket
----@param bucket string
----@param callback fun(err: nil|string)
-function M.mb(bucket, callback)
-  local cmd = create_s3_command({ "mb", bucket })
-  shell.run(cmd, callback)
-end
-
---- Move files
----@param src string
----@param dest string
----@param is_folder boolean
----@param callback fun(err: nil|string)
-function M.mv(src, dest, is_folder, callback)
-  local main_cmd = { "mv", src, dest }
-  if is_folder then
-    table.insert(main_cmd, "--recursive")
-  end
-  local cmd = create_s3_command(main_cmd)
-  shell.run(cmd, callback)
-end
-
---- Copy files
----@param src string
----@param dest string
----@param is_folder boolean
----@param callback fun(err: nil|string)
-function M.cp(src, dest, is_folder, callback)
-  local main_cmd = { "cp", src, dest }
-  if is_folder then
-    table.insert(main_cmd, "--recursive")
-  end
-  local cmd = create_s3_command(main_cmd)
-  shell.run(cmd, callback)
-end
-
-return M
diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua
index ae4291f..c52f5a9 100644
--- a/lua/oil/adapters/ssh.lua
+++ b/lua/oil/adapters/ssh.lua
@@ -50,7 +50,6 @@ M.parse_url = function(oil_url)
     error(string.format("Malformed SSH url: %s", oil_url))
   end
 
-  ---@cast ret oil.sshUrl
   return ret
 end
 
@@ -126,7 +125,7 @@ ssh_columns.permissions = {
 
   compare = function(entry, parsed_value)
     local meta = entry[FIELD_META]
-    if parsed_value and meta and meta.mode then
+    if parsed_value 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
@@ -169,7 +168,7 @@ ssh_columns.size = {
 
   get_sort_value = function(entry)
     local meta = entry[FIELD_META]
-    if meta and meta.size then
+    if meta.size then
       return meta.size
     else
       return 0
@@ -303,8 +302,8 @@ M.perform_action = function(action, cb)
     local conn = get_connection(action.url)
     conn:rm(res.path, cb)
   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))
+    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 = M.parse_url(action.src_url)
       local dest_res = M.parse_url(action.dest_url)
@@ -324,8 +323,8 @@ M.perform_action = function(action, cb)
       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))
+    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 = M.parse_url(action.src_url)
       local dest_res = M.parse_url(action.dest_url)
@@ -441,7 +440,6 @@ M.goto_file = function()
   url.path = vim.fs.dirname(fullpath)
   local parurl = url_to_str(url)
 
-  ---@cast M oil.Adapter
   util.adapter_list_all(M, parurl, {}, function(err, entries)
     if err then
       vim.notify(string.format("Error finding file '%s': %s", fname, err), vim.log.levels.ERROR)
diff --git a/lua/oil/adapters/ssh/connection.lua b/lua/oil/adapters/ssh/connection.lua
index 6a47c07..146b140 100644
--- a/lua/oil/adapters/ssh/connection.lua
+++ b/lua/oil/adapters/ssh/connection.lua
@@ -176,7 +176,6 @@ function SSHConnection.new(url)
     end
   end)
 
-  ---@cast self oil.sshConnection
   return self
 end
 
diff --git a/lua/oil/adapters/ssh/sshfs.lua b/lua/oil/adapters/ssh/sshfs.lua
index 0dcc169..3b9faf3 100644
--- a/lua/oil/adapters/ssh/sshfs.lua
+++ b/lua/oil/adapters/ssh/sshfs.lua
@@ -42,17 +42,10 @@ local function parse_ls_line(line)
   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+(.*)")
-    if name == nil then
-      major, minor, date, name =
-        rem:match("^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
-    end
     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+(.*)")
-    if name == nil then
-      size, date, name = rem:match("^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
-    end
     meta.size = tonumber(size)
   end
   meta.iso_modified_date = date
@@ -77,7 +70,6 @@ end
 ---@param url oil.sshUrl
 ---@return oil.sshFs
 function SSHFS.new(url)
-  ---@type oil.sshFs
   return setmetatable({
     conn = SSHConnection.new(url),
   }, {
diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua
index 10c0749..c6221ab 100644
--- a/lua/oil/adapters/trash/freedesktop.lua
+++ b/lua/oil/adapters/trash/freedesktop.lua
@@ -1,5 +1,5 @@
 -- Based on the FreeDesktop.org trash specification
--- https://specifications.freedesktop.org/trash-spec/1.0/
+-- https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html
 local cache = require("oil.cache")
 local config = require("oil.config")
 local constants = require("oil.constants")
@@ -131,7 +131,7 @@ end
 ---@param cb fun(path: string)
 M.get_entry_path = function(url, entry, cb)
   local internal_entry = assert(cache.get_entry_by_id(entry.id))
-  local meta = assert(internal_entry[FIELD_META])
+  local meta = internal_entry[FIELD_META]
   ---@type oil.TrashInfo
   local trash_info = meta.trash_info
   if not trash_info then
@@ -151,7 +151,7 @@ end
 ---@field info_file string
 ---@field original_path string
 ---@field deletion_date number
----@field stat uv.aliases.fs_stat_table
+---@field stat uv_fs_t
 
 ---@param info_file string
 ---@param cb fun(err?: string, info?: oil.TrashInfo)
@@ -210,7 +210,6 @@ local function read_trash_info(info_file, cb)
               cb(".trashinfo file points to non-existant file")
             else
               trash_info.stat = trash_stat
-              ---@cast trash_info oil.TrashInfo
               cb(nil, trash_info)
             end
           end)
@@ -381,7 +380,7 @@ file_columns.mtime = {
   get_sort_value = function(entry)
     local meta = entry[FIELD_META]
     ---@type nil|oil.TrashInfo
-    local trash_info = meta and meta.trash_info
+    local trash_info = meta.trash_info
     if trash_info then
       return trash_info.deletion_date
     else
@@ -417,7 +416,7 @@ M.filter_action = function(action)
   elseif action.type == "delete" then
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
-    return meta ~= nil and meta.trash_info ~= nil
+    return meta.trash_info ~= nil
   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))
@@ -447,7 +446,7 @@ M.render_action = function(action)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.TrashInfo
-    local trash_info = assert(meta).trash_info
+    local trash_info = meta.trash_info
     local short_path = fs.shorten_path(trash_info.original_path)
     return string.format(" PURGE %s", short_path)
   elseif action.type == "move" then
@@ -561,7 +560,7 @@ M.perform_action = function(action, cb)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.TrashInfo
-    local trash_info = assert(meta).trash_info
+    local trash_info = meta.trash_info
     purge(trash_info, cb)
   elseif action.type == "move" then
     local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
@@ -576,7 +575,7 @@ M.perform_action = function(action, cb)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META]
       ---@type oil.TrashInfo
-      local trash_info = assert(meta).trash_info
+      local trash_info = meta.trash_info
       fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err)
         if err then
           return cb(err)
@@ -596,7 +595,8 @@ M.perform_action = function(action, cb)
         if err then
           cb(err)
         else
-          local stat_type = trash_info.stat.type or "unknown"
+          ---@diagnostic disable-next-line: undefined-field
+          local stat_type = trash_info.stat.type
           fs.recursive_copy(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
         end
       end)
@@ -607,7 +607,7 @@ M.perform_action = function(action, cb)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META]
       ---@type oil.TrashInfo
-      local trash_info = assert(meta).trash_info
+      local trash_info = meta.trash_info
       fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
     else
       error("Must be moving files into or out of trash")
@@ -624,7 +624,8 @@ M.delete_to_trash = function(path, cb)
     if err then
       cb(err)
     else
-      local stat_type = trash_info.stat.type or "unknown"
+      ---@diagnostic disable-next-line: undefined-field
+      local stat_type = trash_info.stat.type
       fs.recursive_move(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
     end
   end)
diff --git a/lua/oil/adapters/trash/mac.lua b/lua/oil/adapters/trash/mac.lua
index 66cf4c1..8b2d33a 100644
--- a/lua/oil/adapters/trash/mac.lua
+++ b/lua/oil/adapters/trash/mac.lua
@@ -224,6 +224,7 @@ M.delete_to_trash = function(path, cb)
       end
 
       local stat_type = src_stat.type
+      ---@cast stat_type oil.EntryType
       fs.recursive_move(stat_type, path, dest, vim.schedule_wrap(cb))
     end)
   )
diff --git a/lua/oil/adapters/trash/windows.lua b/lua/oil/adapters/trash/windows.lua
index f7634e1..ecfca61 100644
--- a/lua/oil/adapters/trash/windows.lua
+++ b/lua/oil/adapters/trash/windows.lua
@@ -37,10 +37,10 @@ local win_addslash = function(path)
 end
 
 ---@class oil.WindowsTrashInfo
----@field trash_file string
----@field original_path string
----@field deletion_date integer
----@field info_file? string
+---@field trash_file string?
+---@field original_path string?
+---@field deletion_date string?
+---@field info_file string?
 
 ---@param url string
 ---@param column_defs string[]
@@ -96,7 +96,6 @@ M.list = function(url, column_defs, cb)
           end
           cache_entry[FIELD_META] = {
             stat = nil,
-            ---@type oil.WindowsTrashInfo
             trash_info = {
               trash_file = entry.Path,
               original_path = entry.OriginalPath,
@@ -165,8 +164,8 @@ file_columns.mtime = {
 
   get_sort_value = function(entry)
     local meta = entry[FIELD_META]
-    ---@type nil|oil.WindowsTrashInfo
-    local trash_info = meta and meta.trash_info
+    ---@type oil.WindowsTrashInfo
+    local trash_info = meta.trash_info
     if trash_info and trash_info.deletion_date then
       return trash_info.deletion_date
     else
@@ -200,7 +199,7 @@ M.filter_action = function(action)
   elseif action.type == "delete" then
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
-    return meta ~= nil and meta.trash_info ~= nil
+    return meta.trash_info ~= nil
   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))
@@ -236,7 +235,7 @@ end
 M.get_entry_path = function(url, entry, cb)
   local internal_entry = assert(cache.get_entry_by_id(entry.id))
   local meta = internal_entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
-  local trash_info = meta and meta.trash_info
+  local trash_info = meta.trash_info
   if not trash_info then
     -- This is a subpath in the trash
     M.normalize_url(url, cb)
@@ -266,7 +265,7 @@ M.render_action = function(action)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.WindowsTrashInfo
-    local trash_info = assert(meta).trash_info
+    local trash_info = meta.trash_info
     local short_path = fs.shorten_path(trash_info.original_path)
     return string.format(" PURGE %s", short_path)
   elseif action.type == "move" then
@@ -349,7 +348,7 @@ M.perform_action = function(action, cb)
   if action.type == "delete" then
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
-    local trash_info = meta and meta.trash_info
+    local trash_info = meta.trash_info
 
     purge(trash_info, cb)
   elseif action.type == "move" then
@@ -365,7 +364,7 @@ M.perform_action = function(action, cb)
       dest_path = fs.posix_to_os_path(dest_path)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
-      local trash_info = meta and meta.trash_info
+      local trash_info = meta.trash_info
       fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err)
         if err then
           return cb(err)
@@ -389,7 +388,7 @@ M.perform_action = function(action, cb)
       dest_path = fs.posix_to_os_path(dest_path)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
-      local trash_info = meta and meta.trash_info
+      local trash_info = meta.trash_info
       fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
     else
       error("Must be moving files into or out of trash")
diff --git a/lua/oil/adapters/trash/windows/powershell-connection.lua b/lua/oil/adapters/trash/windows/powershell-connection.lua
index 332defb..f16c346 100644
--- a/lua/oil/adapters/trash/windows/powershell-connection.lua
+++ b/lua/oil/adapters/trash/windows/powershell-connection.lua
@@ -22,22 +22,11 @@ function PowershellConnection.new(init_command)
 
   self:_init(init_command)
 
-  ---@type oil.PowershellConnection
   return self
 end
 
 ---@param init_command? string
 function PowershellConnection:_init(init_command)
-  -- For some reason beyond my understanding, at least one of the following
-  -- things requires `noshellslash` to avoid the embeded powershell process to
-  -- send only "" to the stdout (never calling the callback because
-  -- "===DONE(True)===" is never sent to stdout)
-  -- * vim.fn.jobstart
-  -- * cmd.exe
-  -- * powershell.exe
-  local saved_shellslash = vim.o.shellslash
-  vim.o.shellslash = false
-
   -- 65001 is the UTF-8 codepage
   -- powershell needs to be launched with the UTF-8 codepage to use it for both stdin and stdout
   local jid = vim.fn.jobstart({
@@ -67,7 +56,6 @@ function PowershellConnection:_init(init_command)
       end
     end,
   })
-  vim.o.shellslash = saved_shellslash
 
   if jid == 0 then
     self:_set_error("passed invalid arguments to 'powershell'")
diff --git a/lua/oil/cache.lua b/lua/oil/cache.lua
index ef5597c..ef78246 100644
--- a/lua/oil/cache.lua
+++ b/lua/oil/cache.lua
@@ -197,7 +197,6 @@ M.perform_action = function(action)
   elseif action.type == "change" then
     -- Cache doesn't need to update
   else
-    ---@diagnostic disable-next-line: undefined-field
     error(string.format("Bad action type: '%s'", action.type))
   end
 end
diff --git a/lua/oil/clipboard.lua b/lua/oil/clipboard.lua
deleted file mode 100644
index 04498f5..0000000
--- a/lua/oil/clipboard.lua
+++ /dev/null
@@ -1,370 +0,0 @@
-local cache = require("oil.cache")
-local columns = require("oil.columns")
-local config = require("oil.config")
-local fs = require("oil.fs")
-local oil = require("oil")
-local parser = require("oil.mutator.parser")
-local util = require("oil.util")
-local view = require("oil.view")
-
-local M = {}
-
----@return "wayland"|"x11"|nil
-local function get_linux_session_type()
-  local xdg_session_type = vim.env.XDG_SESSION_TYPE
-  if not xdg_session_type then
-    return
-  end
-  xdg_session_type = xdg_session_type:lower()
-  if xdg_session_type:find("x11") then
-    return "x11"
-  elseif xdg_session_type:find("wayland") then
-    return "wayland"
-  else
-    return nil
-  end
-end
-
----@return boolean
-local function is_linux_desktop_gnome()
-  local cur_desktop = vim.env.XDG_CURRENT_DESKTOP
-  local session_desktop = vim.env.XDG_SESSION_DESKTOP
-  local idx = session_desktop and session_desktop:lower():find("gnome")
-    or cur_desktop and cur_desktop:lower():find("gnome")
-  return idx ~= nil or cur_desktop == "X-Cinnamon" or cur_desktop == "XFCE"
-end
-
----@param winid integer
----@param entry oil.InternalEntry
----@param column_defs oil.ColumnSpec[]
----@param adapter oil.Adapter
----@param bufnr integer
-local function write_pasted(winid, entry, column_defs, adapter, bufnr)
-  local col_width = {}
-  for i in ipairs(column_defs) do
-    col_width[i + 1] = 1
-  end
-  local line_table =
-    { view.format_entry_cols(entry, column_defs, col_width, adapter, false, bufnr) }
-  local lines, _ = util.render_table(line_table, col_width)
-  local pos = vim.api.nvim_win_get_cursor(winid)
-  vim.api.nvim_buf_set_lines(bufnr, pos[1], pos[1], true, lines)
-end
-
----@param parent_url string
----@param entry oil.InternalEntry
-local function remove_entry_from_parent_buffer(parent_url, entry)
-  local bufnr = vim.fn.bufadd(parent_url)
-  assert(vim.api.nvim_buf_is_loaded(bufnr), "Expected parent buffer to be loaded during paste")
-  local adapter = assert(util.get_adapter(bufnr))
-  local column_defs = columns.get_supported_columns(adapter)
-  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
-  for i, line in ipairs(lines) do
-    local result = parser.parse_line(adapter, line, column_defs)
-    if result and result.entry == entry then
-      vim.api.nvim_buf_set_lines(bufnr, i - 1, i, false, {})
-      return
-    end
-  end
-  local exported = util.export_entry(entry)
-  vim.notify(
-    string.format("Error: could not delete original file '%s'", exported.name),
-    vim.log.levels.ERROR
-  )
-end
-
----@param paths string[]
----@param delete_original? boolean
-local function paste_paths(paths, delete_original)
-  local bufnr = vim.api.nvim_get_current_buf()
-  local scheme = "oil://"
-  local adapter = assert(config.get_adapter_by_scheme(scheme))
-  local column_defs = columns.get_supported_columns(scheme)
-  local winid = vim.api.nvim_get_current_win()
-
-  local parent_urls = {}
-  local pending_paths = {}
-
-  -- Handle as many paths synchronously as possible
-  for _, path in ipairs(paths) do
-    -- Trim the trailing slash off directories
-    if vim.endswith(path, "/") then
-      path = path:sub(1, -2)
-    end
-
-    local ori_entry = cache.get_entry_by_url(scheme .. path)
-    local parent_url = util.addslash(scheme .. vim.fs.dirname(path))
-    if ori_entry then
-      write_pasted(winid, ori_entry, column_defs, adapter, bufnr)
-      if delete_original then
-        remove_entry_from_parent_buffer(parent_url, ori_entry)
-      end
-    else
-      parent_urls[parent_url] = true
-      table.insert(pending_paths, path)
-    end
-  end
-
-  -- If all paths could be handled synchronously, we're done
-  if #pending_paths == 0 then
-    return
-  end
-
-  -- Process the remaining paths by asynchronously loading them
-  local cursor = vim.api.nvim_win_get_cursor(winid)
-  local complete_loading = util.cb_collect(#vim.tbl_keys(parent_urls), function(err)
-    if err then
-      vim.notify(string.format("Error loading parent directory: %s", err), vim.log.levels.ERROR)
-    else
-      -- Something in this process moves the cursor to the top of the window, so have to restore it
-      vim.api.nvim_win_set_cursor(winid, cursor)
-
-      for _, path in ipairs(pending_paths) do
-        local ori_entry = cache.get_entry_by_url(scheme .. path)
-        if ori_entry then
-          write_pasted(winid, ori_entry, column_defs, adapter, bufnr)
-          if delete_original then
-            local parent_url = util.addslash(scheme .. vim.fs.dirname(path))
-            remove_entry_from_parent_buffer(parent_url, ori_entry)
-          end
-        else
-          vim.notify(
-            string.format("The pasted file '%s' could not be found", path),
-            vim.log.levels.ERROR
-          )
-        end
-      end
-    end
-  end)
-
-  for parent_url, _ in pairs(parent_urls) do
-    local new_bufnr = vim.api.nvim_create_buf(false, false)
-    vim.api.nvim_buf_set_name(new_bufnr, parent_url)
-    oil.load_oil_buffer(new_bufnr)
-    util.run_after_load(new_bufnr, complete_loading)
-  end
-end
-
----@return integer start
----@return integer end
-local function range_from_selection()
-  -- [bufnum, lnum, col, off]; both row and column 1-indexed
-  local start = vim.fn.getpos("v")
-  local end_ = vim.fn.getpos(".")
-  local start_row = start[2]
-  local end_row = end_[2]
-
-  if start_row > end_row then
-    start_row, end_row = end_row, start_row
-  end
-
-  return start_row, end_row
-end
-
-M.copy_to_system_clipboard = function()
-  local dir = oil.get_current_dir()
-  if not dir then
-    vim.notify("System clipboard only works for local files", vim.log.levels.ERROR)
-    return
-  end
-
-  local entries = {}
-  local mode = vim.api.nvim_get_mode().mode
-  if mode == "v" or mode == "V" then
-    if fs.is_mac then
-      vim.notify(
-        "Copying multiple paths to clipboard is not supported on mac",
-        vim.log.levels.ERROR
-      )
-      return
-    end
-    local start_row, end_row = range_from_selection()
-    for i = start_row, end_row do
-      table.insert(entries, oil.get_entry_on_line(0, i))
-    end
-
-    -- leave visual mode
-    vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", true)
-  else
-    table.insert(entries, oil.get_cursor_entry())
-  end
-
-  -- This removes holes in the list-like table
-  entries = vim.tbl_values(entries)
-
-  if #entries == 0 then
-    vim.notify("Could not find local file under cursor", vim.log.levels.WARN)
-    return
-  end
-  local paths = {}
-  for _, entry in ipairs(entries) do
-    table.insert(paths, dir .. entry.name)
-  end
-  local cmd = {}
-  local stdin
-  if fs.is_mac then
-    cmd = {
-      "osascript",
-      "-e",
-      "on run args",
-      "-e",
-      "set the clipboard to POSIX file (first item of args)",
-      "-e",
-      "end run",
-      paths[1],
-    }
-  elseif fs.is_linux then
-    local xdg_session_type = get_linux_session_type()
-    if xdg_session_type == "x11" then
-      vim.list_extend(cmd, { "xclip", "-i", "-selection", "clipboard" })
-    elseif xdg_session_type == "wayland" then
-      table.insert(cmd, "wl-copy")
-    else
-      vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR)
-      return
-    end
-    local urls = {}
-    for _, path in ipairs(paths) do
-      table.insert(urls, "file://" .. path)
-    end
-    if is_linux_desktop_gnome() then
-      stdin = string.format("copy\n%s\0", table.concat(urls, "\n"))
-      vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" })
-    else
-      stdin = table.concat(urls, "\n") .. "\n"
-      vim.list_extend(cmd, { "-t", "text/uri-list" })
-    end
-  else
-    vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR)
-    return
-  end
-
-  if vim.fn.executable(cmd[1]) == 0 then
-    vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR)
-    return
-  end
-  local stderr = ""
-  local jid = vim.fn.jobstart(cmd, {
-    stderr_buffered = true,
-    on_stderr = function(_, data)
-      stderr = table.concat(data, "\n")
-    end,
-    on_exit = function(j, exit_code)
-      if exit_code ~= 0 then
-        vim.notify(
-          string.format("Error copying '%s' to system clipboard\n%s", vim.inspect(paths), stderr),
-          vim.log.levels.ERROR
-        )
-      else
-        if #paths == 1 then
-          vim.notify(string.format("Copied '%s' to system clipboard", paths[1]))
-        else
-          vim.notify(string.format("Copied %d files to system clipboard", #paths))
-        end
-      end
-    end,
-  })
-  assert(jid > 0, "Failed to start job")
-  if stdin then
-    vim.api.nvim_chan_send(jid, stdin)
-    vim.fn.chanclose(jid, "stdin")
-  end
-end
-
----@param lines string[]
----@return string[]
-local function handle_paste_output_mac(lines)
-  local ret = {}
-  for _, line in ipairs(lines) do
-    if not line:match("^%s*$") then
-      table.insert(ret, line)
-    end
-  end
-  return ret
-end
-
----@param lines string[]
----@return string[]
-local function handle_paste_output_linux(lines)
-  local ret = {}
-  for _, line in ipairs(lines) do
-    local path = line:match("^file://(.+)$")
-    if path then
-      table.insert(ret, util.url_unescape(path))
-    end
-  end
-  return ret
-end
-
----@param delete_original? boolean Delete the source file after pasting
-M.paste_from_system_clipboard = function(delete_original)
-  local dir = oil.get_current_dir()
-  if not dir then
-    return
-  end
-  local cmd = {}
-  local handle_paste_output
-  if fs.is_mac then
-    cmd = {
-      "osascript",
-      "-e",
-      "on run",
-      "-e",
-      "POSIX path of (the clipboard as «class furl»)",
-      "-e",
-      "end run",
-    }
-    handle_paste_output = handle_paste_output_mac
-  elseif fs.is_linux then
-    local xdg_session_type = get_linux_session_type()
-    if xdg_session_type == "x11" then
-      vim.list_extend(cmd, { "xclip", "-o", "-selection", "clipboard" })
-    elseif xdg_session_type == "wayland" then
-      table.insert(cmd, "wl-paste")
-    else
-      vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR)
-      return
-    end
-    if is_linux_desktop_gnome() then
-      vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" })
-    else
-      vim.list_extend(cmd, { "-t", "text/uri-list" })
-    end
-    handle_paste_output = handle_paste_output_linux
-  else
-    vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR)
-    return
-  end
-  local paths
-  local stderr = ""
-  if vim.fn.executable(cmd[1]) == 0 then
-    vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR)
-    return
-  end
-  local jid = vim.fn.jobstart(cmd, {
-    stdout_buffered = true,
-    stderr_buffered = true,
-    on_stdout = function(j, data)
-      local lines = vim.split(table.concat(data, "\n"), "\r?\n")
-      paths = handle_paste_output(lines)
-    end,
-    on_stderr = function(_, data)
-      stderr = table.concat(data, "\n")
-    end,
-    on_exit = function(j, exit_code)
-      if exit_code ~= 0 or not paths then
-        vim.notify(
-          string.format("Error pasting from system clipboard: %s", stderr),
-          vim.log.levels.ERROR
-        )
-      elseif #paths == 0 then
-        vim.notify("No valid files found in system clipboard", vim.log.levels.WARN)
-      else
-        paste_paths(paths, delete_original)
-      end
-    end,
-  })
-  assert(jid > 0, "Failed to start job")
-end
-
-return M
diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua
index 975576f..40b7d74 100644
--- a/lua/oil/columns.lua
+++ b/lua/oil/columns.lua
@@ -12,13 +12,13 @@ local all_columns = {}
 ---@alias oil.ColumnSpec string|{[1]: string, [string]: any}
 
 ---@class (exact) oil.ColumnDefinition
----@field render fun(entry: oil.InternalEntry, conf: nil|table, bufnr: integer): nil|oil.TextChunk
+---@field render fun(entry: oil.InternalEntry, conf: nil|table): nil|oil.TextChunk
 ---@field parse fun(line: string, conf: nil|table): nil|string, nil|string
+---@field meta_fields? table
 ---@field compare? fun(entry: oil.InternalEntry, parsed_value: any): boolean
 ---@field render_action? fun(action: oil.ChangeAction): string
 ---@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string))
 ---@field get_sort_value? fun(entry: oil.InternalEntry): number|string
----@field create_sort_value_factory? fun(num_entries: integer): fun(entry: oil.InternalEntry): number|string
 
 ---@param name string
 ---@param column oil.ColumnDefinition
@@ -53,16 +53,55 @@ M.get_supported_columns = function(adapter_or_scheme)
   return ret
 end
 
-local EMPTY = { "-", "OilEmpty" }
+---@param adapter oil.Adapter
+---@param column_defs table[]
+---@return fun(parent_url: string, entry: oil.InternalEntry, cb: fun(err: nil|string))
+M.get_metadata_fetcher = function(adapter, column_defs)
+  local keyfetches = {}
+  local num_keys = 0
+  for _, def in ipairs(column_defs) do
+    local name = util.split_config(def)
+    local column = M.get_column(adapter, name)
+    if column and column.meta_fields then
+      for k, v in pairs(column.meta_fields) do
+        if not keyfetches[k] then
+          keyfetches[k] = v
+          num_keys = num_keys + 1
+        end
+      end
+    end
+  end
+  if num_keys == 0 then
+    return function(_, _, cb)
+      cb()
+    end
+  end
+  return function(parent_url, entry, cb)
+    cb = util.cb_collect(num_keys, cb)
+    local meta = {}
+    entry[FIELD_META] = meta
+    for k, v in pairs(keyfetches) do
+      v(parent_url, entry, function(err, value)
+        if err then
+          cb(err)
+        else
+          meta[k] = value
+          cb()
+        end
+      end)
+    end
+  end
+end
+
+local EMPTY = { "-", "Comment" }
 
 M.EMPTY = EMPTY
 
 ---@param adapter oil.Adapter
 ---@param col_def oil.ColumnSpec
 ---@param entry oil.InternalEntry
----@param bufnr integer
 ---@return oil.TextChunk
-M.render_col = function(adapter, col_def, entry, bufnr)
+M.render_col = function(adapter, col_def, entry)
   local name, conf = util.split_config(col_def)
   local column = M.get_column(adapter, name)
   if not column then
@@ -70,7 +109,19 @@ M.render_col = function(adapter, col_def, entry, bufnr)
     return EMPTY
   end
 
-  local chunk = column.render(entry, conf, bufnr)
+  -- Make sure all the required metadata exists before attempting to render
+  if column.meta_fields then
+    local meta = entry[FIELD_META]
+    if not meta then
+      return EMPTY
+    end
+    for k in pairs(column.meta_fields) do
+      if not meta[k] then
+        return EMPTY
+      end
+    end
+  end
+  local chunk = column.render(entry, conf)
   if type(chunk) == "table" then
     if chunk[1]:match("^%s*$") then
       return EMPTY
@@ -98,13 +149,13 @@ end
 M.parse_col = function(adapter, line, col_def)
   local name, conf = util.split_config(col_def)
   -- If rendering failed, there will just be a "-"
-  local empty_col, rem = line:match("^%s*(-%s+)(.*)$")
+  local empty_col, rem = line:match("^(-%s+)(.*)$")
   if empty_col then
     return nil, rem
   end
   local column = M.get_column(adapter, name)
   if column then
-    return column.parse(line:gsub("^%s+", ""), conf)
+    return column.parse(line, conf)
   end
 end
 
@@ -200,7 +251,7 @@ local function is_entry_directory(entry)
     return true
   elseif type == "link" then
     local meta = entry[FIELD_META]
-    return (meta and meta.link_stat and meta.link_stat.type == "directory") == true
+    return meta and meta.link_stat and meta.link_stat.type == "directory"
   else
     return false
   end
@@ -228,8 +279,8 @@ M.register("type", {
   end,
 })
 
-local function adjust_number(int)
-  return string.format("%03d%s", #int, int)
+local function pad_number(int)
+  return string.format("%012d", int)
 end
 
 M.register("name", {
@@ -241,33 +292,18 @@ M.register("name", {
     error("Do not use the name column. It is for sorting only")
   end,
 
-  create_sort_value_factory = function(num_entries)
-    if
-      config.view_options.natural_order == false
-      or (config.view_options.natural_order == "fast" and num_entries > 5000)
-    then
-      if config.view_options.case_insensitive then
-        return function(entry)
-          return entry[FIELD_NAME]:lower()
-        end
-      else
-        return function(entry)
-          return entry[FIELD_NAME]
-        end
-      end
-    else
-      local memo = {}
-      return function(entry)
-        if memo[entry] == nil then
-          local name = entry[FIELD_NAME]:gsub("0*(%d+)", adjust_number)
-          if config.view_options.case_insensitive then
-            name = name:lower()
-          end
-          memo[entry] = name
-        end
-        return memo[entry]
-      end
+  get_sort_value = function(entry)
+    local sort_value = entry[FIELD_NAME]
+
+    if config.view_options.natural_order then
+      sort_value = sort_value:gsub("%d+", pad_number)
     end
+
+    if config.view_options.case_insensitive then
+      sort_value = sort_value:lower()
+    end
+
+    return sort_value
   end,
 })
 
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index cafa783..32da34e 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -1,3 +1,5 @@
+--stylua: ignore
+
 local default_config = {
   -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`)
   -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories.
@@ -38,8 +40,6 @@ local default_config = {
   -- Note that the cleanup process only starts when none of the oil buffers are currently displayed
   cleanup_delay_ms = 2000,
   lsp_file_methods = {
-    -- Enable or disable LSP file operations
-    enabled = true,
     -- Time to wait for LSP file operations to complete before skipping
     timeout_ms = 1000,
     -- Set to true to autosave buffers that are updated with LSP willRenameFiles
@@ -58,22 +58,22 @@ local default_config = {
   -- Set to `false` to remove a keymap
   -- See :help oil-actions for a list of all available actions
   keymaps = {
-    ["g?"] = { "actions.show_help", mode = "n" },
+    ["g?"] = "actions.show_help",
     [""] = "actions.select",
-    [""] = { "actions.select", opts = { vertical = true } },
-    [""] = { "actions.select", opts = { horizontal = true } },
-    [""] = { "actions.select", opts = { tab = true } },
+    [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" },
+    [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" },
+    [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
     [""] = "actions.preview",
-    [""] = { "actions.close", mode = "n" },
+    [""] = "actions.close",
     [""] = "actions.refresh",
-    ["-"] = { "actions.parent", mode = "n" },
-    ["_"] = { "actions.open_cwd", mode = "n" },
-    ["`"] = { "actions.cd", mode = "n" },
-    ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
-    ["gs"] = { "actions.change_sort", mode = "n" },
+    ["-"] = "actions.parent",
+    ["_"] = "actions.open_cwd",
+    ["`"] = "actions.cd",
+    ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" },
+    ["gs"] = "actions.change_sort",
     ["gx"] = "actions.open_external",
-    ["g."] = { "actions.toggle_hidden", mode = "n" },
-    ["g\\"] = { "actions.toggle_trash", mode = "n" },
+    ["g."] = "actions.toggle_hidden",
+    ["g\\"] = "actions.toggle_trash",
   },
   -- Set to false to disable all of the above keymaps
   use_default_keymaps = true,
@@ -82,16 +82,15 @@ local default_config = {
     show_hidden = false,
     -- This function defines what is considered a "hidden" file
     is_hidden_file = function(name, bufnr)
-      local m = name:match("^%.")
-      return m ~= nil
+      return vim.startswith(name, ".")
     end,
     -- This function defines what will never be shown, even when `show_hidden` is set
     is_always_hidden = function(name, bufnr)
       return false
     end,
-    -- Sort file names with numbers in a more intuitive order for humans.
-    -- Can be "fast", true, or false. "fast" will turn it off for large directories.
-    natural_order = "fast",
+    -- Sort file names in a more intuitive order for humans. Is less performant,
+    -- so you may want to set to false if you work with large directories.
+    natural_order = true,
     -- Sort file and directory names case insensitive
     case_insensitive = false,
     sort = {
@@ -100,15 +99,9 @@ local default_config = {
       { "type", "asc" },
       { "name", "asc" },
     },
-    -- Customize the highlight group for the file name
-    highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
-      return nil
-    end,
   },
   -- Extra arguments to pass to SCP when moving/copying files over SSH
   extra_scp_args = {},
-  -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
-  extra_s3_args = {},
   -- EXPERIMENTAL support for performing file operations with git
   git = {
     -- Return true to automatically git add/mv/rm files
@@ -126,15 +119,12 @@ local default_config = {
   float = {
     -- Padding around the floating window
     padding = 2,
-    -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
     max_width = 0,
     max_height = 0,
-    border = nil,
+    border = "rounded",
     win_options = {
       winblend = 0,
     },
-    -- optionally override the oil buffers window title with custom function: fun(winid: integer): string
-    get_win_title = nil,
     -- preview_split: Split direction: "auto", "left", "right", "above", "below".
     preview_split = "auto",
     -- This is the config that will be passed to nvim_open_win.
@@ -143,21 +133,8 @@ local default_config = {
       return conf
     end,
   },
-  -- Configuration for the file preview window
-  preview_win = {
-    -- Whether the preview window is automatically updated when the cursor is moved
-    update_on_cursor_moved = true,
-    -- How to open the preview window "load"|"scratch"|"fast_scratch"
-    preview_method = "fast_scratch",
-    -- A function that returns true to disable preview on a file e.g. to avoid lag
-    disable_preview = function(filename)
-      return false
-    end,
-    -- Window-local options to use for preview window buffers
-    win_options = {},
-  },
-  -- Configuration for the floating action confirmation window
-  confirmation = {
+  -- Configuration for the actions floating preview window
+  preview = {
     -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
     -- min_width and max_width can be a single value or a list of mixed integer/float types.
     -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
@@ -174,10 +151,12 @@ local default_config = {
     min_height = { 5, 0.1 },
     -- optionally define an integer/float for the exact height of the preview window
     height = nil,
-    border = nil,
+    border = "rounded",
     win_options = {
       winblend = 0,
     },
+    -- Whether the preview window is automatically updated when the cursor is moved
+    update_on_cursor_moved = true,
   },
   -- Configuration for the floating progress window
   progress = {
@@ -187,7 +166,7 @@ local default_config = {
     max_height = { 10, 0.9 },
     min_height = { 5, 0.1 },
     height = nil,
-    border = nil,
+    border = "rounded",
     minimized_border = "none",
     win_options = {
       winblend = 0,
@@ -195,35 +174,28 @@ local default_config = {
   },
   -- Configuration for the floating SSH window
   ssh = {
-    border = nil,
+    border = "rounded",
   },
   -- Configuration for the floating keymaps help window
   keymaps_help = {
-    border = nil,
+    border = "rounded",
   },
 }
 
 -- The adapter API hasn't really stabilized yet. We're not ready to advertise or encourage people to
 -- write their own adapters, and so there's no real reason to edit these config options. For that
 -- reason, I'm taking them out of the section above so they won't show up in the autogen docs.
-
--- not "oil-s3://" on older neovim versions, since it doesn't open buffers correctly with a number
--- in the name
-local oil_s3_string = vim.fn.has("nvim-0.12") == 1 and "oil-s3://" or "oil-sss://"
 default_config.adapters = {
   ["oil://"] = "files",
   ["oil-ssh://"] = "ssh",
-  [oil_s3_string] = "s3",
   ["oil-trash://"] = "trash",
 }
 default_config.adapter_aliases = {}
--- We want the function in the default config for documentation generation, but if we nil it out
--- here we can get some performance wins
-default_config.view_options.highlight_filename = nil
 
 ---@class oil.Config
 ---@field adapters table Hidden from SetupOpts
 ---@field adapter_aliases table Hidden from SetupOpts
+---@field trash_command? string Deprecated option that we should clean up soon
 ---@field silence_scp_warning? boolean Undocumented option
 ---@field default_file_explorer boolean
 ---@field columns oil.ColumnSpec[]
@@ -240,11 +212,9 @@ default_config.view_options.highlight_filename = nil
 ---@field use_default_keymaps boolean
 ---@field view_options oil.ViewOptions
 ---@field extra_scp_args string[]
----@field extra_s3_args string[]
 ---@field git oil.GitOptions
 ---@field float oil.FloatWindowConfig
----@field preview_win oil.PreviewWindowConfig
----@field confirmation oil.ConfirmationWindowConfig
+---@field preview oil.PreviewWindowConfig
 ---@field progress oil.ProgressWindowConfig
 ---@field ssh oil.SimpleWindowConfig
 ---@field keymaps_help oil.SimpleWindowConfig
@@ -269,22 +239,18 @@ local M = {}
 ---@field use_default_keymaps? boolean Set to false to disable all of the above keymaps
 ---@field view_options? oil.SetupViewOptions Configure which files are shown and how they are shown.
 ---@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 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
----@field confirmation? oil.SetupConfirmationWindowConfig Configuration for the floating action confirmation window
+---@field preview? oil.SetupPreviewWindowConfig Configuration for the actions floating preview window
 ---@field progress? oil.SetupProgressWindowConfig Configuration for the floating progress window
 ---@field ssh? oil.SetupSimpleWindowConfig Configuration for the floating SSH window
 ---@field keymaps_help? oil.SetupSimpleWindowConfig Configuration for the floating keymaps help window
 
 ---@class (exact) oil.LspFileMethods
----@field enabled boolean
 ---@field timeout_ms integer
 ---@field autosave_changes boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers.
 
 ---@class (exact) oil.SetupLspFileMethods
----@field enabled? boolean Enable or disable LSP file operations
 ---@field timeout_ms? integer Time to wait for LSP file operations to complete before skipping.
 ---@field autosave_changes? boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers.
 
@@ -292,19 +258,17 @@ local M = {}
 ---@field show_hidden boolean
 ---@field is_hidden_file fun(name: string, bufnr: integer): boolean
 ---@field is_always_hidden fun(name: string, bufnr: integer): boolean
----@field natural_order boolean|"fast"
+---@field natural_order boolean
 ---@field case_insensitive boolean
 ---@field sort oil.SortSpec[]
----@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean, bufnr: integer): string|nil
 
 ---@class (exact) oil.SetupViewOptions
 ---@field show_hidden? boolean Show files and directories that start with "."
 ---@field is_hidden_file? fun(name: string, bufnr: integer): boolean This function defines what is considered a "hidden" file
 ---@field is_always_hidden? fun(name: string, bufnr: integer): boolean This function defines what will never be shown, even when `show_hidden` is set
----@field natural_order? boolean|"fast" Sort file names with numbers in a more intuitive order for humans. Can be slow for large directories.
+---@field natural_order? boolean Sort file names in a more intuitive order for humans. Is less performant, so you may want to set to false if you work with large directories.
 ---@field case_insensitive? boolean Sort file and directory names case insensitive
 ---@field sort? oil.SortSpec[] Sort order for the file list
----@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean): string|nil Customize the highlight group for the file name
 
 ---@class (exact) oil.SortSpec
 ---@field [1] string
@@ -346,26 +310,11 @@ local M = {}
 ---@field border? string|string[] Window border
 ---@field win_options? table
 
----@alias oil.PreviewMethod
----| '"load"' # Load the previewed file into a buffer
----| '"scratch"' # Put the text into a scratch buffer to avoid LSP attaching
----| '"fast_scratch"' # Put only the visible text into a scratch buffer
-
----@class (exact) oil.PreviewWindowConfig
+---@class (exact) oil.PreviewWindowConfig : oil.WindowConfig
 ---@field update_on_cursor_moved boolean
----@field preview_method oil.PreviewMethod
----@field disable_preview fun(filename: string): boolean
----@field win_options table
 
----@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig
-
----@class (exact) oil.SetupPreviewWindowConfig
+---@class (exact) oil.SetupPreviewWindowConfig : oil.SetupWindowConfig
 ---@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved
----@field disable_preview? fun(filename: string): boolean A function that returns true to disable preview on a file e.g. to avoid lag
----@field preview_method? oil.PreviewMethod How to open the preview window
----@field win_options? table Window-local options to use for preview window buffers
-
----@class (exact) oil.SetupConfirmationWindowConfig : oil.SetupWindowConfig
 
 ---@class (exact) oil.ProgressWindowConfig : oil.WindowConfig
 ---@field minimized_border string|string[]
@@ -379,7 +328,6 @@ local M = {}
 ---@field max_height integer
 ---@field border string|string[]
 ---@field win_options table
----@field get_win_title fun(winid: integer): string
 ---@field preview_split "auto"|"left"|"right"|"above"|"below"
 ---@field override fun(conf: table): table
 
@@ -389,7 +337,6 @@ local M = {}
 ---@field max_height? integer
 ---@field border? string|string[] Window border
 ---@field win_options? table
----@field get_win_title? fun(winid: integer): string
 ---@field preview_split? "auto"|"left"|"right"|"above"|"below" Direction that the preview command will split the window
 ---@field override? fun(conf: table): table
 
@@ -400,37 +347,9 @@ local M = {}
 ---@field border? string|string[] Window border
 
 M.setup = function(opts)
-  opts = opts or {}
-
-  local new_conf = vim.tbl_deep_extend("keep", opts, default_config)
+  local new_conf = vim.tbl_deep_extend("keep", opts or {}, default_config)
   if not new_conf.use_default_keymaps then
     new_conf.keymaps = opts.keymaps or {}
-  elseif opts.keymaps then
-    -- We don't want to deep merge the keymaps, we want any keymap defined by the user to override
-    -- everything about the default.
-    for k, v in pairs(opts.keymaps) do
-      new_conf.keymaps[k] = v
-    end
-  end
-
-  -- Backwards compatibility for old versions that don't support winborder
-  if vim.fn.has("nvim-0.11") == 0 then
-    new_conf = vim.tbl_deep_extend("keep", new_conf, {
-      float = { border = "rounded" },
-      confirmation = { border = "rounded" },
-      progress = { border = "rounded" },
-      ssh = { border = "rounded" },
-      keymaps_help = { border = "rounded" },
-    })
-  end
-
-  -- Backwards compatibility. We renamed the 'preview' window config to be called 'confirmation'.
-  if opts.preview and not opts.confirmation then
-    new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation)
-  end
-  -- Backwards compatibility. We renamed the 'preview' config to 'preview_win'
-  if opts.preview and opts.preview.update_on_cursor_moved ~= nil then
-    new_conf.preview_win.update_on_cursor_moved = opts.preview.update_on_cursor_moved
   end
 
   if new_conf.lsp_rename_autosave ~= nil then
@@ -476,6 +395,10 @@ M.get_adapter_by_scheme = function(scheme)
   if adapter == nil then
     local name = M.adapters[scheme]
     if not name then
+      vim.notify(
+        string.format("Could not find oil adapter for scheme '%s'", scheme),
+        vim.log.levels.ERROR
+      )
       return nil
     end
     local ok
@@ -486,6 +409,7 @@ M.get_adapter_by_scheme = function(scheme)
     else
       M._adapter_by_scheme[scheme] = false
       adapter = false
+      vim.notify(string.format("Could not find oil adapter '%s'", name), vim.log.levels.ERROR)
     end
   end
   if adapter then
diff --git a/lua/oil/constants.lua b/lua/oil/constants.lua
index 3f5a38a..e1ef56b 100644
--- a/lua/oil/constants.lua
+++ b/lua/oil/constants.lua
@@ -2,7 +2,7 @@ local M = {}
 
 ---Store entries as a list-like table for maximum space efficiency and retrieval speed.
 ---We use the constants below to index into the table.
----@alias oil.InternalEntry {[1]: integer, [2]: string, [3]: oil.EntryType, [4]: nil|table}
+---@alias oil.InternalEntry any[]
 
 -- Indexes into oil.InternalEntry
 M.FIELD_ID = 1
diff --git a/lua/oil/fs.lua b/lua/oil/fs.lua
index f169c0b..ac216a1 100644
--- a/lua/oil/fs.lua
+++ b/lua/oil/fs.lua
@@ -1,4 +1,3 @@
-local log = require("oil.log")
 local M = {}
 
 local uv = vim.uv or vim.loop
@@ -218,7 +217,7 @@ M.recursive_delete = function(entry_type, path, cb)
           local waiting = #entries
           local complete
           complete = function(err2)
-            if err2 then
+            if err then
               complete = function() end
               return inner_cb(err2)
             end
@@ -246,37 +245,6 @@ M.recursive_delete = function(entry_type, path, cb)
   end, 10000)
 end
 
----Move the undofile for the file at src_path to dest_path
----@param src_path string
----@param dest_path string
----@param copy boolean
-local move_undofile = vim.schedule_wrap(function(src_path, dest_path, copy)
-  local undofile = vim.fn.undofile(src_path)
-  uv.fs_stat(
-    undofile,
-    vim.schedule_wrap(function(stat_err)
-      if stat_err then
-        -- undofile doesn't exist
-        return
-      end
-      local dest_undofile = vim.fn.undofile(dest_path)
-      if copy then
-        uv.fs_copyfile(src_path, dest_path, function(err)
-          if err then
-            log.warn("Error copying undofile %s: %s", undofile, err)
-          end
-        end)
-      else
-        uv.fs_rename(undofile, dest_undofile, function(err)
-          if err then
-            log.warn("Error moving undofile %s: %s", undofile, err)
-          end
-        end)
-      end
-    end)
-  )
-end)
-
 ---@param entry_type oil.EntryType
 ---@param src_path string
 ---@param dest_path string
@@ -294,7 +262,6 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
   end
   if entry_type ~= "directory" then
     uv.fs_copyfile(src_path, dest_path, { excl = true }, cb)
-    move_undofile(src_path, dest_path, true)
     return
   end
   uv.fs_stat(src_path, function(stat_err, src_stat)
@@ -320,7 +287,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
               local waiting = #entries
               local complete
               complete = function(err2)
-                if err2 then
+                if err then
                   complete = function() end
                   return inner_cb(err2)
                 end
@@ -366,9 +333,6 @@ M.recursive_move = function(entry_type, src_path, dest_path, cb)
         end
       end)
     else
-      if entry_type ~= "directory" then
-        move_undofile(src_path, dest_path, false)
-      end
       cb()
     end
   end)
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 908d6dd..e8b4ddc 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -7,7 +7,7 @@ local M = {}
 ---@field parsed_name nil|string
 ---@field meta nil|table
 
----@alias oil.EntryType uv.aliases.fs_types
+---@alias oil.EntryType "file"|"directory"|"socket"|"link"|"fifo"
 ---@alias oil.HlRange { [1]: string, [2]: integer, [3]: integer } A tuple of highlight group name, col_start, col_end
 ---@alias oil.HlTuple { [1]: string, [2]: string } A tuple of text, highlight group
 ---@alias oil.HlRangeTuple { [1]: string, [2]: oil.HlRange[] } A tuple of text, internal highlights
@@ -222,7 +222,7 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent)
     if not use_oil_parent then
       return bufname
     end
-    local adapter = assert(config.get_adapter_by_scheme(scheme))
+    local adapter = config.get_adapter_by_scheme(scheme)
     local parent_url
     if adapter and adapter.get_parent then
       local adapter_scheme = config.adapter_to_scheme[adapter.name]
@@ -239,21 +239,18 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent)
   end
 end
 
----@class (exact) oil.OpenOpts
----@field preview? oil.OpenPreviewOpts When present, open the preview window after opening oil
-
 ---Open oil browser in a floating window
----@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
----@param opts? oil.OpenOpts
----@param cb? fun() Called after the oil buffer is ready
-M.open_float = function(dir, opts, cb)
-  opts = opts or {}
+---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
+M.open_float = function(dir)
   local config = require("oil.config")
   local layout = require("oil.layout")
   local util = require("oil.util")
   local view = require("oil.view")
 
   local parent_url, basename = M.get_url_for_path(dir)
+  if not parent_url then
+    return
+  end
   if basename then
     view.set_last_cursor(parent_url, basename)
   end
@@ -291,6 +288,19 @@ M.open_float = function(dir, opts, cb)
     })
   )
 
+  ---Recalculate the window title for the current buffer
+  local function get_title()
+    local src_buf = vim.api.nvim_win_get_buf(winid)
+    local title = vim.api.nvim_buf_get_name(src_buf)
+    local scheme, path = util.parse_url(title)
+    if config.adapters[scheme] == "files" then
+      assert(path)
+      local fs = require("oil.fs")
+      title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~")
+    end
+    return title
+  end
+
   table.insert(
     autocmds,
     vim.api.nvim_create_autocmd("BufWinEnter", {
@@ -314,7 +324,7 @@ M.open_float = function(dir, opts, cb)
             col = cur_win_opts.col,
             width = cur_win_opts.width,
             height = cur_win_opts.height,
-            title = util.get_title(winid),
+            title = get_title(),
           })
         end
       end,
@@ -327,14 +337,6 @@ M.open_float = function(dir, opts, cb)
     vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 })
   end
 
-  util.run_after_load(0, function()
-    if opts.preview then
-      M.open_preview(opts.preview, cb)
-    elseif cb then
-      cb()
-    end
-  end)
-
   if vim.fn.has("nvim-0.9") == 0 then
     util.add_title_to_win(winid)
   end
@@ -342,16 +344,11 @@ end
 
 ---Open oil browser in a floating window, or close it if open
 ---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
----@param opts? oil.OpenOpts
----@param cb? fun() Called after the oil buffer is ready
-M.toggle_float = function(dir, opts, cb)
+M.toggle_float = function(dir)
   if vim.w.is_oil_win then
     M.close()
-    if cb then
-      cb()
-    end
   else
-    M.open_float(dir, opts, cb)
+    M.open_float(dir)
   end
 end
 
@@ -373,15 +370,15 @@ local function update_preview_window(oil_bufnr)
 end
 
 ---Open oil browser for a directory
----@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
----@param opts? oil.OpenOpts
----@param cb? fun() Called after the oil buffer is ready
-M.open = function(dir, opts, cb)
-  opts = opts or {}
+---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
+M.open = function(dir)
   local config = require("oil.config")
   local util = require("oil.util")
   local view = require("oil.view")
   local parent_url, basename = M.get_url_for_path(dir)
+  if not parent_url then
+    return
+  end
   if basename then
     view.set_last_cursor(parent_url, basename)
   end
@@ -391,25 +388,12 @@ M.open = function(dir, opts, cb)
     vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 })
   end
 
-  util.run_after_load(0, function()
-    if opts.preview then
-      M.open_preview(opts.preview, cb)
-    elseif cb then
-      cb()
-    end
-  end)
-
   -- If preview window exists, update its content
   update_preview_window()
 end
 
----@class oil.CloseOpts
----@field exit_if_last_buf? boolean Exit vim if this oil buffer is the last open buffer
-
 ---Restore the buffer that was present when oil was opened
----@param opts? oil.CloseOpts
-M.close = function(opts)
-  opts = opts or {}
+M.close = function()
   -- If we're in a floating oil window, close it and try to restore focus to the original window
   if vim.w.is_oil_win then
     local original_winid = vim.w.oil_original_win
@@ -432,26 +416,18 @@ M.close = function(opts)
   -- buffer first
   local oilbuf = vim.api.nvim_get_current_buf()
   ok = pcall(vim.cmd.bprev)
-  -- If `bprev` failed, there are no buffers open
   if not ok then
-    -- either exit or create a new blank buffer
-    if opts.exit_if_last_buf then
-      vim.cmd.quit()
-    else
-      vim.cmd.enew()
-    end
+    -- If `bprev` failed, there are no buffers open so we should create a new one with enew
+    vim.cmd.enew()
   end
   vim.api.nvim_buf_delete(oilbuf, { force = true })
 end
 
----@class oil.OpenPreviewOpts
----@field vertical? boolean Open the buffer in a vertical split
----@field horizontal? boolean Open the buffer in a horizontal split
----@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
-
 ---Preview the entry under the cursor in a split
----@param opts? oil.OpenPreviewOpts
----@param callback? fun(err: nil|string) Called once the preview window has been opened
+---@param opts nil|table
+---    vertical boolean Open the buffer in a vertical split
+---    horizontal boolean Open the buffer in a horizontal split
+---    split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
 M.open_preview = function(opts, callback)
   opts = opts or {}
   local config = require("oil.config")
@@ -478,7 +454,7 @@ M.open_preview = function(opts, callback)
     end
   end
 
-  local preview_win = util.get_preview_win({ include_not_owned = true })
+  local preview_win = util.get_preview_win()
   local prev_win = vim.api.nvim_get_current_win()
   local bufnr = vim.api.nvim_get_current_buf()
 
@@ -525,7 +501,6 @@ M.open_preview = function(opts, callback)
 
       preview_win = vim.api.nvim_open_win(bufnr, true, win_opts)
       vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = preview_win })
-      vim.api.nvim_win_set_var(preview_win, "oil_preview", true)
       vim.api.nvim_set_current_win(prev_win)
     elseif vim.fn.has("nvim-0.9") == 1 then
       vim.api.nvim_win_set_config(preview_win, { title = entry_title })
@@ -548,8 +523,6 @@ M.open_preview = function(opts, callback)
   end
 
   util.get_edit_path(bufnr, entry, function(normalized_url)
-    local mc = package.loaded["multicursor-nvim"]
-    local has_multicursors = mc and mc.hasCursors()
     local is_visual_mode = util.is_visual_mode()
     if preview_win then
       if is_visual_mode then
@@ -559,29 +532,14 @@ M.open_preview = function(opts, callback)
       end
     end
 
+    local filebufnr = vim.fn.bufadd(normalized_url)
     local entry_is_file = not vim.endswith(normalized_url, "/")
-    local filebufnr
-    if entry_is_file then
-      if config.preview_win.disable_preview(normalized_url) then
-        filebufnr = vim.api.nvim_create_buf(false, true)
-        vim.bo[filebufnr].bufhidden = "wipe"
-        vim.bo[filebufnr].buftype = "nofile"
-        util.render_text(filebufnr, "Preview disabled", { winid = preview_win })
-      elseif
-        config.preview_win.preview_method ~= "load"
-        and not util.file_matches_bufreadcmd(normalized_url)
-      then
-        filebufnr =
-          util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method)
-      end
-    end
 
-    if not filebufnr then
-      filebufnr = vim.fn.bufadd(normalized_url)
-      if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then
-        vim.bo[filebufnr].bufhidden = "wipe"
-        vim.b[filebufnr].oil_preview_buffer = true
-      end
+    -- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after
+    -- we close the window
+    if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then
+      vim.bo[filebufnr].bufhidden = "wipe"
+      vim.b[filebufnr].oil_preview_buffer = true
     end
 
     ---@diagnostic disable-next-line: param-type-mismatch
@@ -595,23 +553,10 @@ M.open_preview = function(opts, callback)
       vim.api.nvim_echo({ { err, "Error" } }, true, {})
     end
 
-    -- If we called open_preview during an autocmd, then the edit command may not trigger the
-    -- BufReadCmd to load the buffer. So we need to do it manually.
-    if util.is_oil_bufnr(filebufnr) then
-      M.load_oil_buffer(filebufnr)
-    end
-
     vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 })
-    vim.api.nvim_win_set_var(0, "oil_preview", true)
-    for k, v in pairs(config.preview_win.win_options) do
-      vim.api.nvim_set_option_value(k, v, { scope = "local", win = preview_win })
-    end
     vim.w.oil_entry_id = entry.id
     vim.w.oil_source_win = prev_win
-    if has_multicursors then
-      hack_set_win(prev_win)
-      mc.restoreCursors()
-    elseif is_visual_mode then
+    if is_visual_mode then
       hack_set_win(prev_win)
       -- Restore the visual selection
       vim.cmd.normal({ args = { "gv" }, bang = true })
@@ -628,7 +573,6 @@ end
 ---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
 ---@field tab? boolean Open the buffer in a new tab
 ---@field close? boolean Close the original oil buffer once selection is made
----@field handle_buffer_callback? fun(buf_id: integer) If defined, all other buffer related options here would be ignored. This callback allows you to take over the process of opening the buffer yourself.
 
 ---Select the entry under the cursor
 ---@param opts nil|oil.SelectOpts
@@ -641,6 +585,14 @@ M.select = function(opts, callback)
   local FIELD_META = constants.FIELD_META
   opts = vim.tbl_extend("keep", opts or {}, {})
 
+  if opts.preview then
+    vim.notify_once(
+      "Deprecated: do not call oil.select with preview=true. Use oil.open_preview instead.\nThis shim will be removed on 2025-01-01"
+    )
+    M.open_preview(opts, callback)
+    return
+  end
+
   local function finish(err)
     if err then
       vim.notify(err, vim.log.levels.ERROR)
@@ -738,7 +690,7 @@ M.select = function(opts, callback)
     else
       -- Close floating window before opening a file
       if vim.w.is_oil_win then
-        M.close()
+        vim.api.nvim_win_close(0, false)
       end
     end
 
@@ -749,7 +701,7 @@ M.select = function(opts, callback)
         vertical = opts.vertical,
         horizontal = opts.horizontal,
         split = opts.split,
-        keepalt = false,
+        keepalt = true,
       }
       local filebufnr = vim.fn.bufadd(normalized_url)
       local entry_is_file = not vim.endswith(normalized_url, "/")
@@ -763,24 +715,18 @@ M.select = function(opts, callback)
       local cmd = "buffer"
       if opts.tab then
         vim.cmd.tabnew({ mods = mods })
-        -- Make sure the new buffer from tabnew gets cleaned up
-        vim.bo.bufhidden = "wipe"
       elseif opts.split then
         cmd = "sbuffer"
       end
-      if opts.handle_buffer_callback ~= nil then
-        opts.handle_buffer_callback(filebufnr)
-      else
-        ---@diagnostic disable-next-line: param-type-mismatch
-        local ok, err = pcall(vim.cmd, {
-          cmd = cmd,
-          args = { filebufnr },
-          mods = mods,
-        })
-        -- Ignore swapfile errors
-        if not ok and err and not err:match("^Vim:E325:") then
-          vim.api.nvim_echo({ { err, "Error" } }, true, {})
-        end
+      ---@diagnostic disable-next-line: param-type-mismatch
+      local ok, err = pcall(vim.cmd, {
+        cmd = cmd,
+        args = { filebufnr },
+        mods = mods,
+      })
+      -- Ignore swapfile errors
+      if not ok and err and not err:match("^Vim:E325:") then
+        vim.api.nvim_echo({ { err, "Error" } }, true, {})
       end
 
       open_next_entry(cb)
@@ -833,26 +779,11 @@ end
 ---@private
 M._get_highlights = function()
   return {
-    {
-      name = "OilEmpty",
-      link = "Comment",
-      desc = "Empty column values",
-    },
-    {
-      name = "OilHidden",
-      link = "Comment",
-      desc = "Hidden entry in an oil buffer",
-    },
     {
       name = "OilDir",
       link = "Directory",
       desc = "Directory names in an oil buffer",
     },
-    {
-      name = "OilDirHidden",
-      link = "OilHidden",
-      desc = "Hidden directory names in an oil buffer",
-    },
     {
       name = "OilDirIcon",
       link = "OilDir",
@@ -863,61 +794,21 @@ M._get_highlights = function()
       link = "Keyword",
       desc = "Socket files in an oil buffer",
     },
-    {
-      name = "OilSocketHidden",
-      link = "OilHidden",
-      desc = "Hidden socket files in an oil buffer",
-    },
     {
       name = "OilLink",
       link = nil,
       desc = "Soft links in an oil buffer",
     },
-    {
-      name = "OilOrphanLink",
-      link = nil,
-      desc = "Orphaned soft links in an oil buffer",
-    },
-    {
-      name = "OilLinkHidden",
-      link = "OilHidden",
-      desc = "Hidden soft links in an oil buffer",
-    },
-    {
-      name = "OilOrphanLinkHidden",
-      link = "OilLinkHidden",
-      desc = "Hidden orphaned soft links in an oil buffer",
-    },
     {
       name = "OilLinkTarget",
       link = "Comment",
       desc = "The target of a soft link",
     },
-    {
-      name = "OilOrphanLinkTarget",
-      link = "DiagnosticError",
-      desc = "The target of an orphaned soft link",
-    },
-    {
-      name = "OilLinkTargetHidden",
-      link = "OilHidden",
-      desc = "The target of a hidden soft link",
-    },
-    {
-      name = "OilOrphanLinkTargetHidden",
-      link = "OilOrphanLinkTarget",
-      desc = "The target of an hidden orphaned soft link",
-    },
     {
       name = "OilFile",
       link = nil,
       desc = "Normal files in an oil buffer",
     },
-    {
-      name = "OilFileHidden",
-      link = "OilHidden",
-      desc = "Hidden normal files in an oil buffer",
-    },
     {
       name = "OilCreate",
       link = "DiagnosticInfo",
@@ -1033,9 +924,8 @@ local function restore_alt_buf()
   end
 end
 
----@private
 ---@param bufnr integer
-M.load_oil_buffer = function(bufnr)
+local function load_oil_buffer(bufnr)
   local config = require("oil.config")
   local keymap_util = require("oil.keymap_util")
   local loading = require("oil.loading")
@@ -1049,11 +939,6 @@ M.load_oil_buffer = function(bufnr)
     util.rename_buffer(bufnr, bufname)
   end
 
-  -- Early return if we're already loading or have already loaded this buffer
-  if loading.is_loading(bufnr) or vim.b[bufnr].filetype ~= nil then
-    return
-  end
-
   local adapter = assert(config.get_adapter_by_scheme(scheme))
 
   if vim.endswith(bufname, "/") then
@@ -1138,14 +1023,13 @@ M.setup = function(opts)
 
   config.setup(opts)
   set_colors()
-  local callback = function(args)
+  vim.api.nvim_create_user_command("Oil", function(args)
     local util = require("oil.util")
-    if args.smods.tab > 0 then
+    if args.smods.tab == 1 then
       vim.cmd.tabnew()
     end
     local float = false
     local trash = false
-    local preview = false
     local i = 1
     while i <= #args.fargs do
       local v = args.fargs[i]
@@ -1155,11 +1039,6 @@ M.setup = function(opts)
       elseif v == "--trash" then
         trash = true
         table.remove(args.fargs, i)
-      elseif v == "--preview" then
-        -- In the future we may want to support specifying options for the preview window (e.g.
-        -- vertical/horizontal), but if you want that level of control maybe just use the API
-        preview = true
-        table.remove(args.fargs, i)
       elseif v == "--progress" then
         local mutator = require("oil.mutator")
         if mutator.is_mutating() then
@@ -1173,34 +1052,23 @@ M.setup = function(opts)
       end
     end
 
-    if not float and (args.smods.vertical or args.smods.horizontal or args.smods.split ~= "") then
-      local range = args.count > 0 and { args.count } or nil
-      local cmdargs = { mods = { split = args.smods.split }, range = range }
+    if not float and (args.smods.vertical or args.smods.split ~= "") then
       if args.smods.vertical then
-        vim.cmd.vsplit(cmdargs)
+        vim.cmd.vsplit({ mods = { split = args.smods.split } })
       else
-        vim.cmd.split(cmdargs)
+        vim.cmd.split({ mods = { split = args.smods.split } })
       end
     end
 
     local method = float and "open_float" or "open"
     local path = args.fargs[1]
-    local open_opts = {}
     if trash then
       local url = M.get_url_for_path(path, false)
       local _, new_path = util.parse_url(url)
       path = "oil-trash://" .. new_path
     end
-    if preview then
-      open_opts.preview = {}
-    end
-    M[method](path, open_opts)
-  end
-  vim.api.nvim_create_user_command(
-    "Oil",
-    callback,
-    { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir", count = true }
-  )
+    M[method](path)
+  end, { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir" })
   local aug = vim.api.nvim_create_augroup("Oil", {})
 
   if config.default_file_explorer then
@@ -1246,7 +1114,7 @@ M.setup = function(opts)
     pattern = scheme_pattern,
     nested = true,
     callback = function(params)
-      M.load_oil_buffer(params.buf)
+      load_oil_buffer(params.buf)
     end,
   })
   vim.api.nvim_create_autocmd("BufWriteCmd", {
@@ -1281,7 +1149,8 @@ M.setup = function(opts)
         end)
         vim.cmd.doautocmd({ args = { "BufWritePost", params.file }, mods = { silent = true } })
       else
-        local adapter = assert(config.get_adapter_by_scheme(bufname))
+        local adapter = config.get_adapter_by_scheme(bufname)
+        assert(adapter)
         adapter.write_file(params.buf)
       end
     end,
@@ -1308,10 +1177,7 @@ M.setup = function(opts)
       local util = require("oil.util")
       local bufname = vim.api.nvim_buf_get_name(0)
       local scheme = util.parse_url(bufname)
-      local is_oil_buf = scheme and config.adapters[scheme]
-      -- We want to filter out oil buffers that are not directories (i.e. ssh files)
-      local is_oil_dir_or_unknown = (vim.bo.filetype == "oil" or vim.bo.filetype == "")
-      if is_oil_buf and is_oil_dir_or_unknown then
+      if scheme and config.adapters[scheme] then
         local view = require("oil.view")
         view.maybe_set_cursor()
         -- While we are in an oil buffer, set the alternate file to the buffer we were in prior to
@@ -1419,7 +1285,7 @@ M.setup = function(opts)
       local util = require("oil.util")
       local scheme = util.parse_url(params.file)
       if config.adapters[scheme] and vim.api.nvim_buf_line_count(params.buf) == 1 then
-        M.load_oil_buffer(params.buf)
+        load_oil_buffer(params.buf)
       end
     end,
   })
@@ -1428,7 +1294,7 @@ M.setup = function(opts)
   if maybe_hijack_directory_buffer(bufnr) and vim.v.vim_did_enter == 1 then
     -- manually call load on a hijacked directory buffer if vim has already entered
     -- (the BufReadCmd will not trigger)
-    M.load_oil_buffer(bufnr)
+    load_oil_buffer(bufnr)
   end
 end
 
diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua
index 8c58738..b94581c 100644
--- a/lua/oil/keymap_util.lua
+++ b/lua/oil/keymap_util.lua
@@ -19,18 +19,7 @@ local function resolve(rhs)
   elseif type(rhs) == "table" then
     local opts = vim.deepcopy(rhs)
     -- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap
-    local callback, parent_opts = resolve(opts.callback or opts[1])
-
-    -- Fall back to the parent desc, adding the opts as a string if it exists
-    if parent_opts.desc and not opts.desc then
-      if opts.opts then
-        opts.desc =
-          string.format("%s %s", parent_opts.desc, vim.inspect(opts.opts):gsub("%s+", " "))
-      else
-        opts.desc = parent_opts.desc
-      end
-    end
-
+    local callback = resolve(opts.callback or opts[1])
     local mode = opts.mode
     if type(rhs.callback) == "string" then
       local action_opts, action_mode
@@ -89,30 +78,31 @@ M.show_help = function(keymaps)
     end
   end
 
+  local col_left = {}
+  local col_desc = {}
   local max_lhs = 1
-  local keymap_entries = {}
   for k, rhs in pairs(keymaps) do
     local all_lhs = lhs_to_all_lhs[k]
     if all_lhs then
       local _, opts = resolve(rhs)
       local keystr = table.concat(all_lhs, "/")
       max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr))
-      table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or "" })
+      table.insert(col_left, { str = keystr, all_lhs = all_lhs })
+      table.insert(col_desc, opts.desc or "")
     end
   end
-  table.sort(keymap_entries, function(a, b)
-    return a.desc < b.desc
-  end)
 
   local lines = {}
   local highlights = {}
   local max_line = 1
-  for _, entry in ipairs(keymap_entries) do
-    local line = string.format(" %s   %s", util.pad_align(entry.str, max_lhs, "left"), entry.desc)
+  for i = 1, #col_left do
+    local left = col_left[i]
+    local desc = col_desc[i]
+    local line = string.format(" %s   %s", util.rpad(left.str, max_lhs), desc)
     max_line = math.max(max_line, vim.api.nvim_strwidth(line))
     table.insert(lines, line)
     local start = 1
-    for _, key in ipairs(entry.all_lhs) do
+    for _, key in ipairs(left.all_lhs) do
       local keywidth = vim.api.nvim_strwidth(key)
       table.insert(highlights, { "Special", #lines, start, start + keywidth })
       start = start + keywidth + 1
diff --git a/lua/oil/layout.lua b/lua/oil/layout.lua
index 6d4563d..fef5f43 100644
--- a/lua/oil/layout.lua
+++ b/lua/oil/layout.lua
@@ -98,7 +98,7 @@ M.calculate_height = function(desired_height, opts)
   )
 end
 
----@class (exact) oil.WinLayout
+---@class (exact) conform.WinLayout
 ---@field width integer
 ---@field height integer
 ---@field row integer
@@ -115,13 +115,11 @@ M.get_fullscreen_win_opts = function()
     width = width - 2 -- The border consumes 1 col on each side
   end
   if config.float.max_width > 0 then
-    local max_width = math.floor(calc_float(config.float.max_width, total_width))
-    width = math.min(width, max_width)
+    width = math.min(width, config.float.max_width)
   end
   local height = total_height - 2 * config.float.padding
   if config.float.max_height > 0 then
-    local max_height = math.floor(calc_float(config.float.max_height, total_height))
-    height = math.min(height, max_height)
+    height = math.min(height, config.float.max_height)
   end
   local row = math.floor((total_height - height) / 2)
   local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width
@@ -141,15 +139,14 @@ end
 ---@param winid integer
 ---@param direction "above"|"below"|"left"|"right"|"auto"
 ---@param gap integer
----@return oil.WinLayout root_dim New dimensions of the original window
----@return oil.WinLayout new_dim New dimensions of the new window
+---@return conform.WinLayout root_dim New dimensions of the original window
+---@return conform.WinLayout new_dim New dimensions of the new window
 M.split_window = function(winid, direction, gap)
   if direction == "auto" then
     direction = vim.o.splitright and "right" or "left"
   end
 
   local float_config = vim.api.nvim_win_get_config(winid)
-  ---@type oil.WinLayout
   local dim_root = {
     width = float_config.width,
     height = float_config.height,
@@ -184,11 +181,6 @@ M.split_window = function(winid, direction, gap)
   return dim_root, dim_new
 end
 
----@param desired_width integer
----@param desired_height integer
----@param opts table
----@return integer width
----@return integer height
 M.calculate_dims = function(desired_width, desired_height, opts)
   local width = M.calculate_width(desired_width, opts)
   local height = M.calculate_height(desired_height, opts)
diff --git a/lua/oil/loading.lua b/lua/oil/loading.lua
index 6e575c5..35a6bba 100644
--- a/lua/oil/loading.lua
+++ b/lua/oil/loading.lua
@@ -74,8 +74,7 @@ M.set_loading = function(bufnr, is_loading)
             M.set_loading(bufnr, false)
             return
           end
-          local lines =
-            { util.pad_align("Loading", math.floor(width / 2) - 3, "right"), bar_iter() }
+          local lines = { util.lpad("Loading", math.floor(width / 2) - 3), bar_iter() }
           util.render_text(bufnr, lines)
         end)
       )
diff --git a/lua/oil/log.lua b/lua/oil/log.lua
deleted file mode 100644
index 28a4f9c..0000000
--- a/lua/oil/log.lua
+++ /dev/null
@@ -1,126 +0,0 @@
-local uv = vim.uv or vim.loop
-local levels_reverse = {}
-for k, v in pairs(vim.log.levels) do
-  levels_reverse[v] = k
-end
-
-local Log = {}
-
----@type integer
-Log.level = vim.log.levels.WARN
-
----@return string
-Log.get_logfile = function()
-  local fs = require("oil.fs")
-
-  local ok, stdpath = pcall(vim.fn.stdpath, "log")
-  if not ok then
-    stdpath = vim.fn.stdpath("cache")
-  end
-  assert(type(stdpath) == "string")
-  return fs.join(stdpath, "oil.log")
-end
-
----@param level integer
----@param msg string
----@param ... any[]
----@return string
-local function format(level, msg, ...)
-  local args = vim.F.pack_len(...)
-  for i = 1, args.n do
-    local v = args[i]
-    if type(v) == "table" then
-      args[i] = vim.inspect(v)
-    elseif v == nil then
-      args[i] = "nil"
-    end
-  end
-  local ok, text = pcall(string.format, msg, vim.F.unpack_len(args))
-  -- TODO figure out how to get formatted time inside luv callback
-  -- local timestr = vim.fn.strftime("%Y-%m-%d %H:%M:%S")
-  local timestr = ""
-  if ok then
-    local str_level = levels_reverse[level]
-    return string.format("%s[%s] %s", timestr, str_level, text)
-  else
-    return string.format(
-      "%s[ERROR] error formatting log line: '%s' args %s",
-      timestr,
-      vim.inspect(msg),
-      vim.inspect(args)
-    )
-  end
-end
-
----@param line string
-local function write(line)
-  -- This will be replaced during initialization
-end
-
-local initialized = false
-local function initialize()
-  if initialized then
-    return
-  end
-  initialized = true
-  local filepath = Log.get_logfile()
-
-  local stat = uv.fs_stat(filepath)
-  if stat and stat.size > 10 * 1024 * 1024 then
-    local backup = filepath .. ".1"
-    uv.fs_unlink(backup)
-    uv.fs_rename(filepath, backup)
-  end
-
-  local parent = vim.fs.dirname(filepath)
-  require("oil.fs").mkdirp(parent)
-
-  local logfile, openerr = io.open(filepath, "a+")
-  if not logfile then
-    local err_msg = string.format("Failed to open oil.nvim log file: %s", openerr)
-    vim.notify(err_msg, vim.log.levels.ERROR)
-  else
-    write = function(line)
-      logfile:write(line)
-      logfile:write("\n")
-      logfile:flush()
-    end
-  end
-end
-
----Override the file handler e.g. for tests
----@param handler fun(line: string)
-function Log.set_handler(handler)
-  write = handler
-  initialized = true
-end
-
-function Log.log(level, msg, ...)
-  if Log.level <= level then
-    initialize()
-    local text = format(level, msg, ...)
-    write(text)
-  end
-end
-
-function Log.trace(...)
-  Log.log(vim.log.levels.TRACE, ...)
-end
-
-function Log.debug(...)
-  Log.log(vim.log.levels.DEBUG, ...)
-end
-
-function Log.info(...)
-  Log.log(vim.log.levels.INFO, ...)
-end
-
-function Log.warn(...)
-  Log.log(vim.log.levels.WARN, ...)
-end
-
-function Log.error(...)
-  Log.log(vim.log.levels.ERROR, ...)
-end
-
-return Log
diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua
index 8e48276..ac8e180 100644
--- a/lua/oil/lsp/workspace.lua
+++ b/lua/oil/lsp/workspace.lua
@@ -68,34 +68,24 @@ local function get_matching_paths(client, filters, paths)
       end
 
       -- Some language servers use forward slashes as path separators on Windows (LuaLS)
-      -- We no longer need this after 0.12: https://github.com/neovim/neovim/commit/322a6d305d088420b23071c227af07b7c1beb41a
-      if vim.fn.has("nvim-0.12") == 0 and fs.is_windows then
+      if fs.is_windows then
         glob = glob:gsub("/", "\\")
       end
 
       ---@type string|vim.lpeg.Pattern
       local glob_to_match = glob
       if vim.glob and vim.glob.to_lpeg then
-        glob = glob:gsub("{(.-)}", function(s)
-          local patterns = vim.split(s, ",")
-          local filtered = {}
-          for _, pat in ipairs(patterns) do
-            if pat ~= "" then
-              table.insert(filtered, pat)
-            end
-          end
-          if #filtered == 0 then
-            return ""
-          end
-          -- HACK around https://github.com/neovim/neovim/issues/28931
-          -- find alternations and sort them by length to try to match the longest first
-          if vim.fn.has("nvim-0.11") == 0 then
-            table.sort(filtered, function(a, b)
+        -- HACK around https://github.com/neovim/neovim/issues/28931
+        -- find alternations and sort them by length to try to match the longest first
+        if vim.fn.has("nvim-0.11") == 0 then
+          glob = glob:gsub("{(.*)}", function(s)
+            local pieces = vim.split(s, ",")
+            table.sort(pieces, function(a, b)
               return a:len() > b:len()
             end)
-          end
-          return "{" .. table.concat(filtered, ",") .. "}"
-        end)
+            return "{" .. table.concat(pieces, ",") .. "}"
+          end)
+        end
 
         glob_to_match = vim.glob.to_lpeg(glob)
       end
@@ -177,13 +167,8 @@ local function will_file_operation(method, capability_name, files, options)
           }
         end, matching_files),
       }
-      local result, err
-      if vim.fn.has("nvim-0.11") == 1 then
-        result, err = client:request_sync(method, params, options.timeout_ms or 1000, 0)
-      else
-        ---@diagnostic disable-next-line: param-type-mismatch
-        result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0)
-      end
+      ---@diagnostic disable-next-line: invisible
+      local result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0)
       if result and result.result then
         if options.apply_edits ~= false then
           vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding)
@@ -219,12 +204,8 @@ local function did_file_operation(method, capability_name, files)
           }
         end, matching_files),
       }
-      if vim.fn.has("nvim-0.11") == 1 then
-        client:notify(method, params)
-      else
-        ---@diagnostic disable-next-line: param-type-mismatch
-        client.notify(method, params)
-      end
+      ---@diagnostic disable-next-line: invisible
+      client.notify(method, params)
     end
   end
 end
@@ -299,15 +280,9 @@ function M.will_rename_files(files, options)
           }
         end, matching_files),
       }
-      local result, err
-      if vim.fn.has("nvim-0.11") == 1 then
-        result, err =
-          client:request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
-      else
-        result, err =
-          ---@diagnostic disable-next-line: param-type-mismatch
-          client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
-      end
+      local result, err =
+        ---@diagnostic disable-next-line: invisible
+        client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
       if result and result.result then
         if options.apply_edits ~= false then
           vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding)
@@ -338,12 +313,8 @@ function M.did_rename_files(files)
           }
         end, matching_files),
       }
-      if vim.fn.has("nvim-0.11") == 1 then
-        client:notify(ms.workspace_didRenameFiles, params)
-      else
-        ---@diagnostic disable-next-line: param-type-mismatch
-        client.notify(ms.workspace_didRenameFiles, params)
-      end
+      ---@diagnostic disable-next-line: invisible
+      client.notify(ms.workspace_didRenameFiles, params)
     end
   end
 end
diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua
index f55957d..ab24a91 100644
--- a/lua/oil/mutator/init.lua
+++ b/lua/oil/mutator/init.lua
@@ -3,12 +3,12 @@ local Trie = require("oil.mutator.trie")
 local cache = require("oil.cache")
 local columns = require("oil.columns")
 local config = require("oil.config")
-local confirmation = require("oil.mutator.confirmation")
 local constants = require("oil.constants")
 local fs = require("oil.fs")
 local lsp_helpers = require("oil.lsp.helpers")
 local oil = require("oil")
 local parser = require("oil.mutator.parser")
+local preview = require("oil.mutator.preview")
 local util = require("oil.util")
 local view = require("oil.view")
 local M = {}
@@ -85,7 +85,7 @@ M.create_actions_from_diffs = function(all_diffs)
     end
   end
   for bufnr, diffs in pairs(all_diffs) do
-    local adapter = util.get_adapter(bufnr, true)
+    local adapter = util.get_adapter(bufnr)
     if not adapter then
       error("Missing adapter")
     end
@@ -140,7 +140,6 @@ M.create_actions_from_diffs = function(all_diffs)
       else
         local by_id = diff_by_id[diff.id]
         -- HACK: set has_delete field on a list-like table of diffs
-        ---@diagnostic disable-next-line: inject-field
         by_id.has_delete = true
         -- Don't insert the delete. We already know that there is a delete because of the presence
         -- in the diff_by_id map. The list will only include the 'new' diffs.
@@ -390,11 +389,7 @@ M.process_actions = function(actions, cb)
     "User",
     { pattern = "OilActionsPre", modeline = false, data = { actions = actions } }
   )
-
-  local did_complete = nil
-  if config.lsp_file_methods.enabled then
-    did_complete = lsp_helpers.will_perform_file_operations(actions)
-  end
+  local did_complete = lsp_helpers.will_perform_file_operations(actions)
 
   -- Convert some cross-adapter moves to a copy + delete
   for _, action in ipairs(actions) do
@@ -402,7 +397,6 @@ M.process_actions = function(actions, cb)
       local _, cross_action = util.get_adapter_for_action(action)
       -- Only do the conversion if the cross-adapter support is "copy"
       if cross_action == "copy" then
-        ---@diagnostic disable-next-line: assign-type-mismatch
         action.type = "copy"
         table.insert(actions, {
           type = "delete",
@@ -448,9 +442,7 @@ M.process_actions = function(actions, cb)
       return
     end
     if idx > #actions then
-      if did_complete then
-        did_complete()
-      end
+      did_complete()
       finish()
       return
     end
@@ -519,7 +511,7 @@ M.try_write_changes = function(confirm, cb)
     if vim.bo[bufnr].modified then
       local diffs, errors = parser.parse(bufnr)
       all_diffs[bufnr] = diffs
-      local adapter = assert(util.get_adapter(bufnr, true))
+      local adapter = assert(util.get_adapter(bufnr))
       if adapter.filter_error then
         errors = vim.tbl_filter(adapter.filter_error, errors)
       end
@@ -553,15 +545,10 @@ M.try_write_changes = function(confirm, cb)
         { all_errors[curbuf][1].lnum + 1, all_errors[curbuf][1].col }
       )
     else
-      local bufnr, errs = next(all_errors)
-      assert(bufnr)
-      assert(errs)
-      -- HACK: This is a workaround for the fact that we can't switch buffers in the middle of a
-      -- BufWriteCmd.
-      vim.schedule(function()
-        vim.api.nvim_win_set_buf(0, bufnr)
-        pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col })
-      end)
+      ---@diagnostic disable-next-line: param-type-mismatch
+      local bufnr, errs = next(pairs(all_errors))
+      vim.api.nvim_win_set_buf(0, bufnr)
+      pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col })
     end
     unlock()
     cb("Error parsing oil buffers")
@@ -569,7 +556,7 @@ M.try_write_changes = function(confirm, cb)
   end
 
   local actions = M.create_actions_from_diffs(all_diffs)
-  confirmation.show(actions, confirm, function(proceed)
+  preview.show(actions, confirm, function(proceed)
     if not proceed then
       unlock()
       cb("Canceled")
diff --git a/lua/oil/mutator/parser.lua b/lua/oil/mutator/parser.lua
index bb22274..d2d3590 100644
--- a/lua/oil/mutator/parser.lua
+++ b/lua/oil/mutator/parser.lua
@@ -95,7 +95,7 @@ M.parse_line = function(adapter, line, column_defs)
     local name = util.split_config(def)
     local range = { start }
     local start_len = string.len(rem)
-    value, rem = columns.parse_col(adapter, assert(rem), def)
+    value, rem = columns.parse_col(adapter, rem, def)
     if not rem then
       return nil, string.format("Parsing %s failed", name)
     end
@@ -156,7 +156,7 @@ M.parse = function(bufnr)
   ---@type oil.ParseError[]
   local errors = {}
   local bufname = vim.api.nvim_buf_get_name(bufnr)
-  local adapter = util.get_adapter(bufnr, true)
+  local adapter = util.get_adapter(bufnr)
   if not adapter then
     table.insert(errors, {
       lnum = 0,
diff --git a/lua/oil/mutator/confirmation.lua b/lua/oil/mutator/preview.lua
similarity index 95%
rename from lua/oil/mutator/confirmation.lua
rename to lua/oil/mutator/preview.lua
index 8bc8020..3f8d87d 100644
--- a/lua/oil/mutator/confirmation.lua
+++ b/lua/oil/mutator/preview.lua
@@ -82,8 +82,6 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
     else
       line = adapter.render_action(action)
     end
-    -- We can't handle lines with newlines in them
-    line = line:gsub("\n", "")
     table.insert(lines, line)
     local line_width = vim.api.nvim_strwidth(line)
     if line_width > max_line_width then
@@ -93,7 +91,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
   table.insert(lines, "")
 
   -- Create the floating window
-  local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation)
+  local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.preview)
   local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, {
     relative = "editor",
     width = width,
@@ -102,7 +100,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
     col = math.floor((layout.get_editor_width() - width) / 2),
     zindex = 152, -- render on top of the floating window title
     style = "minimal",
-    border = config.confirmation.border,
+    border = config.preview.border,
   })
   if not ok then
     vim.notify(string.format("Error showing oil preview window: %s", winid), vim.log.levels.ERROR)
@@ -110,7 +108,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
   end
   vim.bo[bufnr].filetype = "oil_preview"
   vim.bo[bufnr].syntax = "oil_preview"
-  for k, v in pairs(config.confirmation.win_options) do
+  for k, v in pairs(config.preview.win_options) do
     vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
   end
 
@@ -157,7 +155,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
     vim.api.nvim_create_autocmd("VimResized", {
       callback = function()
         if vim.api.nvim_win_is_valid(winid) then
-          width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation)
+          width, height = layout.calculate_dims(max_line_width, #lines, config.preview)
           vim.api.nvim_win_set_config(winid, {
             relative = "editor",
             width = width,
diff --git a/lua/oil/mutator/trie.lua b/lua/oil/mutator/trie.lua
index 7bac161..ea39266 100644
--- a/lua/oil/mutator/trie.lua
+++ b/lua/oil/mutator/trie.lua
@@ -7,7 +7,6 @@ local Trie = {}
 
 ---@return oil.Trie
 Trie.new = function()
-  ---@type oil.Trie
   return setmetatable({
     root = { values = {}, children = {} },
   }, {
diff --git a/lua/oil/shell.lua b/lua/oil/shell.lua
index b04b27b..2c401ef 100644
--- a/lua/oil/shell.lua
+++ b/lua/oil/shell.lua
@@ -26,8 +26,7 @@ M.run = function(cmd, opts, callback)
           if err == "" then
             err = "Unknown error"
           end
-          local cmd_str = type(cmd) == "string" and cmd or table.concat(cmd, " ")
-          callback(string.format("Error running command '%s'\n%s", cmd_str, err))
+          callback(err)
         end
       end),
     })
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 0ec1acd..aa717ad 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -21,67 +21,50 @@ end
 ---@param filename string
 ---@return string
 M.escape_filename = function(filename)
-  local ret = vim.fn.fnameescape(filename)
+  local ret = filename:gsub("([%%#$])", "\\%1")
   return ret
 end
 
-local _url_escape_to_char = {
-  ["20"] = " ",
-  ["22"] = "“",
-  ["23"] = "#",
-  ["24"] = "$",
-  ["25"] = "%",
-  ["26"] = "&",
-  ["27"] = "‘",
-  ["2B"] = "+",
-  ["2C"] = ",",
-  ["2F"] = "/",
-  ["3A"] = ":",
-  ["3B"] = ";",
-  ["3C"] = "<",
-  ["3D"] = "=",
-  ["3E"] = ">",
-  ["3F"] = "?",
-  ["40"] = "@",
-  ["5B"] = "[",
-  ["5C"] = "\\",
-  ["5D"] = "]",
-  ["5E"] = "^",
-  ["60"] = "`",
-  ["7B"] = "{",
-  ["7C"] = "|",
-  ["7D"] = "}",
-  ["7E"] = "~",
+local _url_escape_chars = {
+  [" "] = "%20",
+  ["$"] = "%24",
+  ["&"] = "%26",
+  ["`"] = "%60",
+  [":"] = "%3A",
+  ["<"] = "%3C",
+  ["="] = "%3D",
+  [">"] = "%3E",
+  ["?"] = "%3F",
+  ["["] = "%5B",
+  ["\\"] = "%5C",
+  ["]"] = "%5D",
+  ["^"] = "%5E",
+  ["{"] = "%7B",
+  ["|"] = "%7C",
+  ["}"] = "%7D",
+  ["~"] = "%7E",
+  ["“"] = "%22",
+  ["‘"] = "%27",
+  ["+"] = "%2B",
+  [","] = "%2C",
+  ["#"] = "%23",
+  ["%"] = "%25",
+  ["@"] = "%40",
+  ["/"] = "%2F",
+  [";"] = "%3B",
 }
-local _char_to_url_escape = {}
-for k, v in pairs(_url_escape_to_char) do
-  _char_to_url_escape[v] = "%" .. k
-end
--- TODO this uri escape handling is very incomplete
-
 ---@param string string
 ---@return string
 M.url_escape = function(string)
-  return (string:gsub(".", _char_to_url_escape))
-end
-
----@param string string
----@return string
-M.url_unescape = function(string)
-  return (
-    string:gsub("%%([0-9A-Fa-f][0-9A-Fa-f])", function(seq)
-      return _url_escape_to_char[seq:upper()] or ("%" .. seq)
-    end)
-  )
+  return (string:gsub(".", _url_escape_chars))
 end
 
 ---@param bufnr integer
----@param silent? boolean
 ---@return nil|oil.Adapter
-M.get_adapter = function(bufnr, silent)
+M.get_adapter = function(bufnr)
   local bufname = vim.api.nvim_buf_get_name(bufnr)
   local adapter = config.get_adapter_by_scheme(bufname)
-  if not adapter and not silent then
+  if not adapter then
     vim.notify_once(
       string.format("[oil] could not find adapter for buffer '%s://'", bufname),
       vim.log.levels.ERROR
@@ -91,28 +74,34 @@ M.get_adapter = function(bufnr, silent)
 end
 
 ---@param text string
----@param width integer|nil
----@param align oil.ColumnAlign
----@return string padded_text
----@return integer left_padding
-M.pad_align = function(text, width, align)
-  if not width then
-    return text, 0
+---@param length nil|integer
+---@return string
+M.rpad = function(text, length)
+  if not length then
+    return text
   end
-  local text_width = vim.api.nvim_strwidth(text)
-  local total_pad = width - text_width
-  if total_pad <= 0 then
-    return text, 0
-  end
-
-  if align == "right" then
-    return string.rep(" ", total_pad) .. text, total_pad
-  elseif align == "center" then
-    local left_pad = math.floor(total_pad / 2)
-    local right_pad = total_pad - left_pad
-    return string.rep(" ", left_pad) .. text .. string.rep(" ", right_pad), left_pad
+  local textlen = vim.api.nvim_strwidth(text)
+  local delta = length - textlen
+  if delta > 0 then
+    return text .. string.rep(" ", delta)
   else
-    return text .. string.rep(" ", total_pad), 0
+    return text
+  end
+end
+
+---@param text string
+---@param length nil|integer
+---@return string
+M.lpad = function(text, length)
+  if not length then
+    return text
+  end
+  local textlen = vim.api.nvim_strwidth(text)
+  local delta = length - textlen
+  if delta > 0 then
+    return string.rep(" ", delta) .. text
+  else
+    return text
   end
 end
 
@@ -168,10 +157,8 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
     -- 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
-      -- Renaming the buffer creates a new buffer with the old name.
-      -- Find it and try to delete it, but don't if the buffer is in a context
-      -- where Neovim doesn't allow buffer modifications.
-      pcall(vim.api.nvim_buf_delete, vim.fn.bufadd(bufname), {})
+      -- Renaming the buffer creates a new buffer with the old name. Find it and delete it.
+      vim.api.nvim_buf_delete(vim.fn.bufadd(bufname), {})
       if altbuf and vim.api.nvim_buf_is_valid(altbuf) then
         vim.fn.setreg("#", altbuf)
       end
@@ -208,18 +195,6 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
       -- Try to delete, but don't if the buffer has changes
       pcall(vim.api.nvim_buf_delete, src_bufnr, {})
     end
-    -- Renaming a buffer won't load the undo file, so we need to do that manually
-    if vim.bo[dest_bufnr].undofile then
-      vim.api.nvim_buf_call(dest_bufnr, function()
-        vim.cmd.rundo({
-          args = { vim.fn.undofile(dest_buf_name) },
-          magic = { file = false, bar = false },
-          mods = {
-            emsg_silent = true,
-          },
-        })
-      end)
-    end
   end)
   return true
 end
@@ -308,15 +283,11 @@ M.split_config = function(name_or_config)
   end
 end
 
----@alias oil.ColumnAlign "left"|"center"|"right"
-
 ---@param lines oil.TextChunk[][]
 ---@param col_width integer[]
----@param col_align? oil.ColumnAlign[]
 ---@return string[]
 ---@return any[][] List of highlights {group, lnum, col_start, col_end}
-M.render_table = function(lines, col_width, col_align)
-  col_align = col_align or {}
+M.render_table = function(lines, col_width)
   local str_lines = {}
   local highlights = {}
   for _, cols in ipairs(lines) do
@@ -330,12 +301,9 @@ M.render_table = function(lines, col_width, col_align)
       else
         text = chunk
       end
-
-      local unpadded_len = text:len()
-      local padding
-      text, padding = M.pad_align(text, col_width[i], col_align[i] or "left")
-
+      text = M.rpad(text, col_width[i])
       table.insert(pieces, text)
+      local col_end = col + text:len() + 1
       if hl then
         if type(hl) == "table" then
           -- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[]
@@ -345,15 +313,15 @@ M.render_table = function(lines, col_width, col_align)
             table.insert(highlights, {
               sub_hl[1],
               #str_lines,
-              col + padding + sub_hl[2],
-              col + padding + sub_hl[3],
+              col + sub_hl[2],
+              col + sub_hl[3],
             })
           end
         else
-          table.insert(highlights, { hl, #str_lines, col + padding, col + padding + unpadded_len })
+          table.insert(highlights, { hl, #str_lines, col, col_end })
         end
       end
-      col = col + text:len() + 1
+      col = col_end
     end
     table.insert(str_lines, table.concat(pieces, " "))
   end
@@ -366,12 +334,7 @@ M.set_highlights = function(bufnr, highlights)
   local ns = vim.api.nvim_create_namespace("Oil")
   vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
   for _, hl in ipairs(highlights) do
-    local group, line, col_start, col_end = unpack(hl)
-    vim.api.nvim_buf_set_extmark(bufnr, ns, line, col_start, {
-      end_col = col_end,
-      hl_group = group,
-      strict = false,
-    })
+    vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl))
   end
 end
 
@@ -384,8 +347,7 @@ M.addslash = function(path, os_slash)
     slash = "\\"
   end
 
-  local endslash = path:match(slash .. "$")
-  if not endslash then
+  if not vim.endswith(path, slash) then
     return path .. slash
   else
     return path
@@ -398,26 +360,6 @@ M.is_floating_win = function(winid)
   return vim.api.nvim_win_get_config(winid or 0).relative ~= ""
 end
 
----Recalculate the window title for the current buffer
----@param winid nil|integer
----@return string
-M.get_title = function(winid)
-  if config.float.get_win_title ~= nil then
-    return config.float.get_win_title(winid or 0)
-  end
-
-  local src_buf = vim.api.nvim_win_get_buf(winid or 0)
-  local title = vim.api.nvim_buf_get_name(src_buf)
-  local scheme, path = M.parse_url(title)
-
-  if config.adapters[scheme] == "files" then
-    assert(path)
-    local fs = require("oil.fs")
-    title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~")
-  end
-  return title
-end
-
 local winid_map = {}
 M.add_title_to_win = function(winid, opts)
   opts = opts or {}
@@ -425,10 +367,21 @@ M.add_title_to_win = function(winid, opts)
   if not vim.api.nvim_win_is_valid(winid) then
     return
   end
+  local function get_title()
+    local src_buf = vim.api.nvim_win_get_buf(winid)
+    local title = vim.api.nvim_buf_get_name(src_buf)
+    local scheme, path = M.parse_url(title)
+    if config.adapters[scheme] == "files" then
+      assert(path)
+      local fs = require("oil.fs")
+      title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~")
+    end
+    return title
+  end
   -- HACK to force the parent window to position itself
   -- See https://github.com/neovim/neovim/issues/13403
   vim.cmd.redraw()
-  local title = M.get_title(winid)
+  local title = get_title()
   local width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(title))
   local title_winid = winid_map[winid]
   local bufnr
@@ -476,7 +429,7 @@ M.add_title_to_win = function(winid, opts)
         if vim.api.nvim_win_get_buf(winid) ~= winbuf then
           return
         end
-        local new_title = M.get_title(winid)
+        local new_title = get_title()
         local new_width =
           math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(new_title))
         vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. new_title .. " " })
@@ -525,7 +478,10 @@ end
 ---@return oil.Adapter
 ---@return nil|oil.CrossAdapterAction
 M.get_adapter_for_action = function(action)
-  local adapter = assert(config.get_adapter_by_scheme(action.url or action.src_url))
+  local adapter = config.get_adapter_by_scheme(action.url or action.src_url)
+  if not adapter then
+    error("no adapter found")
+  end
   if action.dest_url then
     local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
     if adapter ~= dest_adapter then
@@ -646,7 +602,11 @@ M.render_text = function(bufnr, text, opts)
   pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
   vim.bo[bufnr].modifiable = false
   vim.bo[bufnr].modified = false
-  M.set_highlights(bufnr, highlights)
+  local ns = vim.api.nvim_create_namespace("Oil")
+  vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
+  for _, hl in ipairs(highlights) do
+    vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl))
+  end
 end
 
 ---Run a function in the context of a full-editor window
@@ -674,12 +634,8 @@ end
 ---@param bufnr integer
 ---@return boolean
 M.is_oil_bufnr = function(bufnr)
-  local filetype = vim.bo[bufnr].filetype
-  if filetype == "oil" then
+  if vim.bo[bufnr].filetype == "oil" then
     return true
-  elseif filetype ~= "" then
-    -- If the filetype is set and is NOT "oil", then it's not an oil buffer
-    return false
   end
   local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr))
   return config.adapters[scheme] or config.adapter_aliases[scheme]
@@ -702,17 +658,10 @@ M.hack_around_termopen_autocmd = function(prev_mode)
   end, 10)
 end
 
----@param opts? {include_not_owned?: boolean}
 ---@return nil|integer
-M.get_preview_win = function(opts)
-  opts = opts or {}
-
+M.get_preview_win = function()
   for _, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
-    if
-      vim.api.nvim_win_is_valid(winid)
-      and vim.wo[winid].previewwindow
-      and (opts.include_not_owned or vim.w[winid]["oil_preview"])
-    then
+    if vim.api.nvim_win_is_valid(winid) and vim.wo[winid].previewwindow then
       return winid
     end
   end
@@ -787,7 +736,7 @@ end
 
 ---Send files from the current oil directory to quickfix
 ---based on the provided options.
----@param opts {target?: "qflist"|"loclist", action?: "r"|"a", only_matching_search?: boolean}
+---@param opts {target?: "qflist"|"loclist", mode?: "r"|"a"}
 M.send_to_quickfix = function(opts)
   if type(opts) ~= "table" then
     opts = {}
@@ -801,11 +750,10 @@ M.send_to_quickfix = function(opts)
   if not range then
     range = { start_lnum = 1, end_lnum = vim.fn.line("$") }
   end
-  local match_all = not opts.only_matching_search
   local qf_entries = {}
   for i = range.start_lnum, range.end_lnum do
     local entry = oil.get_entry_on_line(0, i)
-    if entry and entry.type == "file" and (match_all or M.is_matching(entry)) then
+    if entry and entry.type == "file" then
       local qf_entry = {
         filename = dir .. entry.name,
         lnum = 1,
@@ -821,13 +769,11 @@ M.send_to_quickfix = function(opts)
   end
   vim.api.nvim_exec_autocmds("QuickFixCmdPre", {})
   local qf_title = "oil files"
-  local action = opts.action == "a" and "a" or "r"
+  local mode = opts.mode == "a" and "a" or "r"
   if opts.target == "loclist" then
-    vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries })
-    vim.cmd.lopen()
+    vim.fn.setloclist(0, {}, mode, { title = qf_title, items = qf_entries })
   else
-    vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries })
-    vim.cmd.copen()
+    vim.fn.setqflist({}, mode, { title = qf_title, items = qf_entries })
   end
   vim.api.nvim_exec_autocmds("QuickFixCmdPost", {})
 end
@@ -854,19 +800,6 @@ M.get_visual_range = function()
   return { start_lnum = start_lnum, end_lnum = end_lnum }
 end
 
----@param entry oil.Entry
----@return boolean
-M.is_matching = function(entry)
-  -- if search highlightig is not enabled, all files are considered to match
-  local search_highlighting_is_off = (vim.v.hlsearch == 0)
-  if search_highlighting_is_off then
-    return true
-  end
-  local pattern = vim.fn.getreg("/")
-  local position_of_match = vim.fn.match(entry.name, pattern)
-  return position_of_match ~= -1
-end
-
 ---@param bufnr integer
 ---@param callback fun()
 M.run_after_load = function(bufnr, callback)
@@ -910,7 +843,7 @@ M.get_edit_path = function(bufnr, entry, callback)
 
   local bufname = vim.api.nvim_buf_get_name(bufnr)
   local scheme, dir = M.parse_url(bufname)
-  local adapter = M.get_adapter(bufnr, true)
+  local adapter = M.get_adapter(bufnr)
   assert(scheme and dir and adapter)
 
   local url = scheme .. dir .. entry.name
@@ -954,85 +887,4 @@ M.get_icon_provider = function()
   end
 end
 
----Read a buffer into a scratch buffer and apply syntactic highlighting when possible
----@param path string The path to the file to read
----@param preview_method oil.PreviewMethod
----@return nil|integer
-M.read_file_to_scratch_buffer = function(path, preview_method)
-  local bufnr = vim.api.nvim_create_buf(false, true)
-  if bufnr == 0 then
-    return
-  end
-
-  vim.bo[bufnr].bufhidden = "wipe"
-  vim.bo[bufnr].buftype = "nofile"
-
-  local has_lines, read_res
-  if preview_method == "fast_scratch" then
-    has_lines, read_res = pcall(vim.fn.readfile, path, "", vim.o.lines)
-  else
-    has_lines, read_res = pcall(vim.fn.readfile, path)
-  end
-  local lines = has_lines and vim.split(table.concat(read_res, "\n"), "\n") or {}
-
-  local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
-  if not ok then
-    return
-  end
-  local ft = vim.filetype.match({ filename = path, buf = bufnr })
-  if ft and ft ~= "" and vim.treesitter.language.get_lang then
-    local lang = vim.treesitter.language.get_lang(ft)
-    if not pcall(vim.treesitter.start, bufnr, lang) then
-      vim.bo[bufnr].syntax = ft
-    else
-    end
-  end
-
-  -- Replace the scratch buffer with a real buffer if we enter it
-  vim.api.nvim_create_autocmd("BufEnter", {
-    desc = "oil.nvim replace scratch buffer with real buffer",
-    buffer = bufnr,
-    callback = function()
-      local winid = vim.api.nvim_get_current_win()
-      -- Have to schedule this so all the FileType, etc autocmds will fire
-      vim.schedule(function()
-        if vim.api.nvim_get_current_win() == winid then
-          vim.cmd.edit({ args = { path } })
-
-          -- If we're still in a preview window, make sure this buffer still gets treated as a
-          -- preview
-          if vim.wo.previewwindow then
-            vim.bo.bufhidden = "wipe"
-            vim.b.oil_preview_buffer = true
-          end
-        end
-      end)
-    end,
-  })
-
-  return bufnr
-end
-
-local _regcache = {}
----Check if a file matches a BufReadCmd autocmd
----@param filename string
----@return boolean
-M.file_matches_bufreadcmd = function(filename)
-  local autocmds = vim.api.nvim_get_autocmds({
-    event = "BufReadCmd",
-  })
-  for _, au in ipairs(autocmds) do
-    local pat = _regcache[au.pattern]
-    if not pat then
-      pat = vim.fn.glob2regpat(au.pattern)
-      _regcache[au.pattern] = pat
-    end
-
-    if vim.fn.match(filename, pat) >= 0 then
-      return true
-    end
-  end
-  return false
-end
-
 return M
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index b3a216e..89e4b8f 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -19,16 +19,10 @@ local last_cursor_entry = {}
 
 ---@param name string
 ---@param bufnr integer
----@return boolean display
----@return boolean is_hidden Whether the file is classified as a hidden file
+---@return boolean
 M.should_display = function(name, bufnr)
-  if config.view_options.is_always_hidden(name, bufnr) then
-    return false, true
-  else
-    local is_hidden = config.view_options.is_hidden_file(name, bufnr)
-    local display = config.view_options.show_hidden or not is_hidden
-    return display, is_hidden
-  end
+  return not config.view_options.is_always_hidden(name, bufnr)
+    and (config.view_options.show_hidden or not config.view_options.is_hidden_file(name, bufnr))
 end
 
 ---@param bufname string
@@ -146,7 +140,7 @@ M.unlock_buffers = function()
   buffers_locked = false
   for bufnr in pairs(session) do
     if vim.api.nvim_buf_is_loaded(bufnr) then
-      local adapter = util.get_adapter(bufnr, true)
+      local adapter = util.get_adapter(bufnr)
       if adapter then
         vim.bo[bufnr].modifiable = adapter.is_modifiable(bufnr)
       end
@@ -185,18 +179,9 @@ end
 
 M.set_win_options = function()
   local winid = vim.api.nvim_get_current_win()
-
-  -- work around https://github.com/neovim/neovim/pull/27422
-  vim.api.nvim_set_option_value("foldmethod", "manual", { scope = "local", win = winid })
-
   for k, v in pairs(config.win_options) do
     vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
   end
-  if vim.wo[winid].previewwindow then -- apply preview window options last
-    for k, v in pairs(config.preview_win.win_options) do
-      vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
-    end
-  end
 end
 
 ---Get a list of visible oil buffers and a list of hidden oil buffers
@@ -257,65 +242,35 @@ local function get_first_mutable_column_col(adapter, ranges)
   return min_col
 end
 
---- @param bufnr integer
---- @param adapter oil.Adapter
---- @param mode false|"name"|"editable"
---- @param cur integer[]
---- @return integer[] | nil
-local function calc_constrained_cursor_pos(bufnr, adapter, mode, cur)
-  local parser = require("oil.mutator.parser")
-  local line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1]
-  local column_defs = columns.get_supported_columns(adapter)
-  local result = parser.parse_line(adapter, line, column_defs)
-  if result and result.ranges then
-    local min_col
-    if mode == "editable" then
-      min_col = get_first_mutable_column_col(adapter, result.ranges)
-    elseif mode == "name" then
-      min_col = result.ranges.name[1]
-    else
-      error(string.format('Unexpected value "%s" for option constrain_cursor', mode))
-    end
-    if cur[2] < min_col then
-      return { cur[1], min_col }
-    end
-  end
-end
-
 ---Force cursor to be after hidden/immutable columns
----@param bufnr integer
----@param mode false|"name"|"editable"
-local function constrain_cursor(bufnr, mode)
-  if not mode then
-    return
-  end
-  if bufnr ~= vim.api.nvim_get_current_buf() then
+local function constrain_cursor()
+  if not config.constrain_cursor then
     return
   end
+  local parser = require("oil.mutator.parser")
 
-  local adapter = util.get_adapter(bufnr, true)
+  local adapter = util.get_adapter(0)
   if not adapter then
     return
   end
 
-  local mc = package.loaded["multicursor-nvim"]
-  if mc then
-    mc.onSafeState(function()
-      mc.action(function(ctx)
-        ctx:forEachCursor(function(cursor)
-          local new_cur =
-            calc_constrained_cursor_pos(bufnr, adapter, mode, { cursor:line(), cursor:col() - 1 })
-          if new_cur then
-            cursor:setPos({ new_cur[1], new_cur[2] + 1 })
-          end
-        end)
-      end)
-    end, { once = true })
-  else
-    local cur = vim.api.nvim_win_get_cursor(0)
-    local new_cur = calc_constrained_cursor_pos(bufnr, adapter, mode, cur)
-    if new_cur then
-      vim.api.nvim_win_set_cursor(0, new_cur)
+  local cur = vim.api.nvim_win_get_cursor(0)
+  local line = vim.api.nvim_buf_get_lines(0, cur[1] - 1, cur[1], true)[1]
+  local column_defs = columns.get_supported_columns(adapter)
+  local result = parser.parse_line(adapter, line, column_defs)
+  if result and result.ranges then
+    local min_col
+    if config.constrain_cursor == "editable" then
+      min_col = get_first_mutable_column_col(adapter, result.ranges)
+    elseif config.constrain_cursor == "name" then
+      min_col = result.ranges.name[1]
+    else
+      error(
+        string.format('Unexpected value "%s" for option constrain_cursor', config.constrain_cursor)
+      )
+    end
+    if cur[2] < min_col then
+      vim.api.nvim_win_set_cursor(0, { cur[1], min_col })
     end
   end
 end
@@ -327,7 +282,7 @@ local function redraw_trash_virtual_text(bufnr)
     return
   end
   local parser = require("oil.mutator.parser")
-  local adapter = util.get_adapter(bufnr, true)
+  local adapter = util.get_adapter(bufnr)
   if not adapter or adapter.name ~= "trash" then
     return
   end
@@ -437,7 +392,7 @@ M.initialize = function(bufnr)
     callback = function()
       -- For some reason the cursor bounces back to its original position,
       -- so we have to defer the call
-      vim.schedule_wrap(constrain_cursor)(bufnr, config.constrain_cursor)
+      vim.schedule(constrain_cursor)
     end,
   })
   vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
@@ -450,15 +405,15 @@ M.initialize = function(bufnr)
         return
       end
 
-      constrain_cursor(bufnr, config.constrain_cursor)
+      constrain_cursor()
 
-      if config.preview_win.update_on_cursor_moved then
+      if config.preview.update_on_cursor_moved then
         -- Debounce and update the preview window
         if timer then
           timer:again()
           return
         end
-        timer = uv.new_timer()
+        timer = vim.loop.new_timer()
         if not timer then
           return
         end
@@ -487,7 +442,7 @@ M.initialize = function(bufnr)
     end,
   })
 
-  local adapter = util.get_adapter(bufnr, true)
+  local adapter = util.get_adapter(bufnr)
 
   -- Set up a watcher that will refresh the directory
   if
@@ -577,9 +532,8 @@ M.initialize = function(bufnr)
 end
 
 ---@param adapter oil.Adapter
----@param num_entries integer
 ---@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean
-local function get_sort_function(adapter, num_entries)
+local function get_sort_function(adapter)
   local idx_funs = {}
   local sort_config = config.view_options.sort
 
@@ -601,9 +555,7 @@ local function get_sort_function(adapter, num_entries)
       )
     end
     local col = columns.get_column(adapter, col_name)
-    if col and col.create_sort_value_factory then
-      table.insert(idx_funs, { col.create_sort_value_factory(num_entries), order })
-    elseif col and col.get_sort_value then
+    if col and col.get_sort_value then
       table.insert(idx_funs, { col.get_sort_value, order })
     else
       vim.notify_once(
@@ -614,7 +566,7 @@ local function get_sort_function(adapter, num_entries)
   end
   return function(a, b)
     for _, sort_fn in ipairs(idx_funs) do
-      local get_sort_value, order = sort_fn[1], sort_fn[2]
+      local get_sort_value, order = unpack(sort_fn)
       local a_val = get_sort_value(a)
       local b_val = get_sort_value(b)
       if a_val ~= b_val then
@@ -647,17 +599,14 @@ local function render_buffer(bufnr, opts)
     jump_first = false,
   })
   local scheme = util.parse_url(bufname)
-  local adapter = util.get_adapter(bufnr, true)
+  local adapter = util.get_adapter(bufnr)
   if not scheme or not adapter then
     return false
   end
   local entries = cache.list_url(bufname)
   local entry_list = vim.tbl_values(entries)
 
-  -- Only sort the entries once we have them all
-  if not vim.b[bufnr].oil_rendering then
-    table.sort(entry_list, get_sort_function(adapter, #entry_list))
-  end
+  table.sort(entry_list, get_sort_function(adapter))
 
   local jump_idx
   if opts.jump_first then
@@ -668,34 +617,30 @@ local function render_buffer(bufnr, opts)
   local column_defs = columns.get_supported_columns(scheme)
   local line_table = {}
   local col_width = {}
-  local col_align = {}
-  for i, col_def in ipairs(column_defs) do
+  for i in ipairs(column_defs) do
     col_width[i + 1] = 1
-    local _, conf = util.split_config(col_def)
-    col_align[i + 1] = conf and conf.align or "left"
   end
 
   if M.should_display("..", bufnr) then
-    local cols =
-      M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter, true, bufnr)
+    local cols = M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter)
     table.insert(line_table, cols)
   end
 
   for _, entry in ipairs(entry_list) do
-    local should_display, is_hidden = M.should_display(entry[FIELD_NAME], bufnr)
-    if should_display then
-      local cols = M.format_entry_cols(entry, column_defs, col_width, adapter, is_hidden, bufnr)
+    if M.should_display(entry[FIELD_NAME], bufnr) then
+      local cols = M.format_entry_cols(entry, column_defs, col_width, adapter)
       table.insert(line_table, cols)
 
       local name = entry[FIELD_NAME]
       if seek_after_render == name then
         seek_after_render_found = true
         jump_idx = #line_table
+        M.set_last_cursor(bufname, nil)
       end
     end
   end
 
-  local lines, highlights = util.render_table(line_table, col_width, col_align)
+  local lines, highlights = util.render_table(line_table, col_width)
 
   vim.bo[bufnr].modifiable = true
   vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
@@ -708,23 +653,19 @@ local function render_buffer(bufnr, opts)
     vim.schedule(function()
       for _, winid in ipairs(vim.api.nvim_list_wins()) do
         if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then
-          if jump_idx then
-            local lnum = jump_idx
-            local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
-            local id_str = line:match("^/(%d+)")
-            local id = tonumber(id_str)
-            if id then
-              local entry = cache.get_entry_by_id(id)
-              if entry then
-                local name = entry[FIELD_NAME]
-                local col = line:find(name, 1, true) or (id_str:len() + 1)
-                vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 })
-                return
-              end
+          -- If we're not jumping to a specific lnum, use the current lnum so we can adjust the col
+          local lnum = jump_idx or vim.api.nvim_win_get_cursor(winid)[1]
+          local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
+          local id_str = line:match("^/(%d+)")
+          local id = tonumber(id_str)
+          if id then
+            local entry = cache.get_entry_by_id(id)
+            if entry then
+              local name = entry[FIELD_NAME]
+              local col = line:find(name, 1, true) or (id_str:len() + 1)
+              vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 })
             end
           end
-
-          constrain_cursor(bufnr, "name")
         end
       end
     end)
@@ -732,48 +673,18 @@ local function render_buffer(bufnr, opts)
   return seek_after_render_found
 end
 
----@param name string
----@param meta? table
----@return string filename
----@return string|nil link_target
-local function get_link_text(name, meta)
-  local link_text
-  if meta then
-    if meta.link_stat and meta.link_stat.type == "directory" then
-      name = name .. "/"
-    end
-
-    if meta.link then
-      link_text = "-> " .. meta.link
-      if meta.link_stat and meta.link_stat.type == "directory" then
-        link_text = util.addslash(link_text)
-      end
-    end
-  end
-
-  return name, link_text
-end
-
 ---@private
 ---@param entry oil.InternalEntry
 ---@param column_defs table[]
 ---@param col_width integer[]
 ---@param adapter oil.Adapter
----@param is_hidden boolean
----@param bufnr integer
 ---@return oil.TextChunk[]
-M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden, bufnr)
+M.format_entry_cols = function(entry, column_defs, col_width, adapter)
   local name = entry[FIELD_NAME]
   local meta = entry[FIELD_META]
-  local hl_suffix = ""
-  if is_hidden then
-    hl_suffix = "Hidden"
-  end
   if meta and meta.display_name then
     name = meta.display_name
   end
-  -- We can't handle newlines in filenames (and shame on you for doing that)
-  name = name:gsub("\n", "")
   -- First put the unique ID
   local cols = {}
   local id_key = cache.format_id(entry[FIELD_ID])
@@ -781,7 +692,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
   table.insert(cols, id_key)
   -- Then add all the configured columns
   for i, column in ipairs(column_defs) do
-    local chunk = columns.render_col(adapter, column, entry, bufnr)
+    local chunk = columns.render_col(adapter, column, entry)
     local text = type(chunk) == "table" and chunk[1] or chunk
     ---@cast text string
     col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text))
@@ -789,59 +700,32 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
   end
   -- Always add the entry name at the end
   local entry_type = entry[FIELD_TYPE]
-
-  local get_custom_hl = config.view_options.highlight_filename
-  local link_name, link_name_hl, link_target, link_target_hl
-  if get_custom_hl then
-    local external_entry = util.export_entry(entry)
-
-    if entry_type == "link" then
-      link_name, link_target = get_link_text(name, meta)
-      local is_orphan = not (meta and meta.link_stat)
-      link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan, bufnr)
-
-      if link_target then
-        link_target_hl = get_custom_hl(external_entry, is_hidden, true, is_orphan, bufnr)
-      end
-
-      -- intentional fallthrough
-    else
-      local hl = get_custom_hl(external_entry, is_hidden, false, false, bufnr)
-      if hl then
-        -- Add the trailing / if this is a directory, this is important
-        if entry_type == "directory" then
-          name = name .. "/"
-        end
-        table.insert(cols, { name, hl })
-        return cols
-      end
-    end
-  end
-
   if entry_type == "directory" then
-    table.insert(cols, { name .. "/", "OilDir" .. hl_suffix })
+    table.insert(cols, { name .. "/", "OilDir" })
   elseif entry_type == "socket" then
-    table.insert(cols, { name, "OilSocket" .. hl_suffix })
+    table.insert(cols, { name, "OilSocket" })
   elseif entry_type == "link" then
-    if not link_name then
-      link_name, link_target = get_link_text(name, meta)
-    end
-    local is_orphan = not (meta and meta.link_stat)
-    if not link_name_hl then
-      link_name_hl = (is_orphan and "OilOrphanLink" or "OilLink") .. hl_suffix
-    end
-    table.insert(cols, { link_name, link_name_hl })
-
-    if link_target then
-      if not link_target_hl then
-        link_target_hl = (is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget") .. hl_suffix
+    local link_text
+    if meta then
+      if meta.link_stat and meta.link_stat.type == "directory" then
+        name = name .. "/"
       end
-      table.insert(cols, { link_target, link_target_hl })
+
+      if meta.link then
+        link_text = "->" .. " " .. meta.link
+        if meta.link_stat and meta.link_stat.type == "directory" then
+          link_text = util.addslash(link_text)
+        end
+      end
+    end
+
+    table.insert(cols, { name, "OilLink" })
+    if link_text then
+      table.insert(cols, { link_text, "OilLinkTarget" })
     end
   else
-    table.insert(cols, { name, "OilFile" .. hl_suffix })
+    table.insert(cols, { name, "OilFile" })
   end
-
   return cols
 end
 
@@ -911,7 +795,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
     handle_error(string.format("Could not parse oil url '%s'", bufname))
     return
   end
-  local adapter = util.get_adapter(bufnr, true)
+  local adapter = util.get_adapter(bufnr)
   if not adapter then
     handle_error(string.format("[oil] no adapter for buffer '%s'", bufname))
     return
@@ -930,7 +814,6 @@ M.render_buffer_async = function(bufnr, opts, callback)
     vim.b[bufnr].oil_rendering = false
     loading.set_loading(bufnr, false)
     render_buffer(bufnr, { jump = true })
-    M.set_last_cursor(bufname, nil)
     vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" })
     vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr)
     if callback then
@@ -955,7 +838,6 @@ M.render_buffer_async = function(bufnr, opts, callback)
   end
 
   cache.begin_update_url(bufname)
-  local num_iterations = 0
   adapter.list(bufname, get_used_columns(), function(err, entries, fetch_more)
     loading.set_loading(bufnr, false)
     if err then
@@ -972,13 +854,11 @@ M.render_buffer_async = function(bufnr, opts, callback)
       local now = uv.hrtime() / 1e6
       local delta = now - start_ms
       -- If we've been chugging for more than 40ms, go ahead and render what we have
-      if (delta > 25 and num_iterations < 1) or delta > 500 then
-        num_iterations = num_iterations + 1
+      if delta > 40 then
         start_ms = now
         vim.schedule(function()
           seek_after_render_found =
             render_buffer(bufnr, { jump = not seek_after_render_found, jump_first = first })
-          start_ms = uv.hrtime() / 1e6
         end)
       end
       first = false
diff --git a/perf/bootstrap.lua b/perf/bootstrap.lua
deleted file mode 100644
index 5f10c06..0000000
--- a/perf/bootstrap.lua
+++ /dev/null
@@ -1,63 +0,0 @@
-vim.opt.runtimepath:prepend("scripts/benchmark.nvim")
-vim.opt.runtimepath:prepend(".")
-
-local bm = require("benchmark")
-bm.sandbox()
-
----@module 'oil'
----@type oil.SetupOpts
-local setup_opts = {
-  -- columns = { "icon", "permissions", "size", "mtime" },
-}
-
-local DIR_SIZE = tonumber(vim.env.DIR_SIZE) or 100000
-local ITERATIONS = tonumber(vim.env.ITERATIONS) or 10
-local WARM_UP = tonumber(vim.env.WARM_UP) or 1
-local OUTLIERS = tonumber(vim.env.OUTLIERS) or math.floor(ITERATIONS / 10)
-local TEST_DIR = "perf/tmp/test_" .. DIR_SIZE
-
-vim.fn.mkdir(TEST_DIR, "p")
-require("benchmark.files").create_files(TEST_DIR, "file %d.txt", DIR_SIZE)
-
-function _G.jit_profile()
-  require("oil").setup(setup_opts)
-  local finish = bm.jit_profile({ filename = TEST_DIR .. "/profile.txt" })
-  bm.wait_for_user_event("OilEnter", function()
-    finish()
-  end)
-  require("oil").open(TEST_DIR)
-end
-
-function _G.flame_profile()
-  local start, stop = bm.flame_profile({
-    pattern = "oil*",
-    filename = "profile.json",
-  })
-  require("oil").setup(setup_opts)
-  start()
-  bm.wait_for_user_event("OilEnter", function()
-    stop(function()
-      vim.cmd.qall({ mods = { silent = true } })
-    end)
-  end)
-  require("oil").open(TEST_DIR)
-end
-
-function _G.benchmark()
-  require("oil").setup(setup_opts)
-  bm.run({ title = "oil.nvim", iterations = ITERATIONS, warm_up = WARM_UP }, function(callback)
-    bm.wait_for_user_event("OilEnter", callback)
-    require("oil").open(TEST_DIR)
-  end, function(times)
-    local avg = bm.avg(times, { trim_outliers = OUTLIERS })
-    local std_dev = bm.std_dev(times, { trim_outliers = OUTLIERS })
-    local lines = {
-      table.concat(vim.tbl_map(bm.format_time, times), " "),
-      string.format("Average: %s", bm.format_time(avg)),
-      string.format("Std deviation: %s", bm.format_time(std_dev)),
-    }
-
-    vim.fn.writefile(lines, "perf/tmp/benchmark.txt")
-    vim.cmd.qall({ mods = { silent = true } })
-  end)
-end
diff --git a/run_tests.sh b/run_tests.sh
index 3018bc0..98b4fa7 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/bin/bash
 set -e
 
 mkdir -p ".testenv/config/nvim"
diff --git a/scripts/generate.py b/scripts/generate.py
index 4e02550..8875e9f 100755
--- a/scripts/generate.py
+++ b/scripts/generate.py
@@ -110,16 +110,11 @@ class ColumnDef:
     params: List["LuaParam"] = field(default_factory=list)
 
 
-UNIVERSAL = [
+HL = [
     LuaParam(
         "highlight",
         "string|fun(value: string): string",
         "Highlight group, or function that returns a highlight group",
-    ),
-    LuaParam(
-        "align",
-        '"left"|"center"|"right"',
-        "Text alignment within the column",
     )
 ]
 TIME = [
@@ -132,7 +127,7 @@ COL_DEFS = [
         False,
         True,
         "The type of the entry (file, directory, link, etc)",
-        UNIVERSAL
+        HL
         + [LuaParam("icons", "table", "Mapping of entry type to icon")],
     ),
     ColumnDef(
@@ -141,7 +136,7 @@ COL_DEFS = [
         False,
         False,
         "An icon for the entry's type (requires nvim-web-devicons)",
-        UNIVERSAL
+        HL
         + [
             LuaParam(
                 "default_file",
@@ -156,31 +151,31 @@ COL_DEFS = [
             ),
         ],
     ),
-    ColumnDef("size", "files, ssh, s3", False, True, "The size of the file", UNIVERSAL + []),
+    ColumnDef("size", "files, ssh", False, True, "The size of the file", HL + []),
     ColumnDef(
         "permissions",
         "files, ssh",
         True,
         False,
         "Access permissions of the file",
-        UNIVERSAL + [],
+        HL + [],
     ),
     ColumnDef(
-        "ctime", "files", False, True, "Change timestamp of the file", UNIVERSAL + TIME + []
+        "ctime", "files", False, True, "Change timestamp of the file", HL + TIME + []
     ),
     ColumnDef(
-        "mtime", "files", False, True, "Last modified time of the file", UNIVERSAL + TIME + []
+        "mtime", "files", False, True, "Last modified time of the file", HL + TIME + []
     ),
     ColumnDef(
-        "atime", "files", False, True, "Last access time of the file", UNIVERSAL + TIME + []
+        "atime", "files", False, True, "Last access time of the file", HL + TIME + []
     ),
     ColumnDef(
         "birthtime",
-        "files, s3",
+        "files",
         False,
         True,
         "The time the file was created",
-        UNIVERSAL + TIME + [],
+        HL + TIME + [],
     ),
 ]
 
@@ -356,13 +351,13 @@ of being permanently deleted. You can browse the trash for a directory using
 the `toggle_trash` action (bound to `g\\` by default). You can view all files
 in the trash with `:Oil --trash /`.
 
-To restore files, simply move them from the trash to the desired destination,
-the same as any other file operation. If you delete files from the trash they
-will be permanently deleted (purged).
+To restore files, simply delete them from the trash and put them in the desired
+destination, the same as any other file operation. If you delete files from the
+trash they will be permanently deleted (purged).
 
 Linux:
     Oil supports the FreeDesktop trash specification.
-    https://specifications.freedesktop.org/trash-spec/1.0/
+    https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html
     All features should work.
 
 Mac:
@@ -371,7 +366,7 @@ Mac:
     (instead of being able to see files that were trashed from a directory).
 
 Windows:
-    Oil supports the Windows Recycle Bin. All features should work.
+    Oil does not yet support the Windows trash. PRs are welcome!
 """
     )
     return section
diff --git a/scripts/requirements.txt b/scripts/requirements.txt
deleted file mode 100644
index 2c6271f..0000000
--- a/scripts/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-pyparsing==3.0.9
-black
-isort
-mypy
diff --git a/syntax/oil_preview.vim b/syntax/oil_preview.vim
index 2f14df9..b6c2fab 100644
--- a/syntax/oil_preview.vim
+++ b/syntax/oil_preview.vim
@@ -2,9 +2,9 @@ if exists("b:current_syntax")
   finish
 endif
 
-syn match oilCreate /^CREATE\( BUCKET\)\? /
+syn match oilCreate /^CREATE /
 syn match oilMove   /^  MOVE /
-syn match oilDelete /^DELETE\( BUCKET\)\? /
+syn match oilDelete /^DELETE /
 syn match oilCopy   /^  COPY /
 syn match oilChange /^CHANGE /
 " Trash operations
diff --git a/tests/altbuf_spec.lua b/tests/altbuf_spec.lua
index beece15..4b51604 100644
--- a/tests/altbuf_spec.lua
+++ b/tests/altbuf_spec.lua
@@ -141,17 +141,5 @@ a.describe("Alternate buffer", function()
       oil.close()
       assert.equals("foo", vim.fn.expand("#"))
     end)
-
-    a.it("preserves alternate when traversing to a new file", function()
-      vim.cmd.edit({ args = { "foo" } })
-      oil.open_float()
-      test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
-      assert.equals("foo", vim.fn.expand("#"))
-      test_util.feedkeys({ "/LICENSE" }, 10)
-      oil.select()
-      test_util.wait_for_autocmd("BufEnter")
-      assert.equals("LICENSE", vim.fn.expand("%:."))
-      assert.equals("foo", vim.fn.expand("#"))
-    end)
   end)
 end)
diff --git a/tests/files_spec.lua b/tests/files_spec.lua
index 4333d80..b268b4c 100644
--- a/tests/files_spec.lua
+++ b/tests/files_spec.lua
@@ -150,10 +150,10 @@ a.describe("files adapter", function()
   a.it("Editing a new oil://path/ creates an oil buffer", function()
     local tmpdir_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "/"
     vim.cmd.edit({ args = { tmpdir_url } })
-    test_util.wait_oil_ready()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     local new_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "newdir"
     vim.cmd.edit({ args = { new_url } })
-    test_util.wait_oil_ready()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     assert.equals("oil", vim.bo.filetype)
     -- The normalization will add a '/'
     assert.equals(new_url .. "/", vim.api.nvim_buf_get_name(0))
@@ -168,6 +168,5 @@ a.describe("files adapter", function()
     test_util.wait_for_autocmd("BufReadPost")
     assert.equals("ruby", vim.bo.filetype)
     assert.equals(vim.fn.fnamemodify(tmpdir.path, ":p") .. "file.rb", vim.api.nvim_buf_get_name(0))
-    assert.equals(tmpdir.path .. "/file.rb", vim.fn.bufname())
   end)
 end)
diff --git a/tests/parser_spec.lua b/tests/parser_spec.lua
index 9884ca1..527e821 100644
--- a/tests/parser_spec.lua
+++ b/tests/parser_spec.lua
@@ -90,7 +90,7 @@ describe("parser", function()
     local file = test_adapter.test_set("/foo/a.txt", "file")
     vim.cmd.edit({ args = { "oil-test:///foo/" } })
     local bufnr = vim.api.nvim_get_current_buf()
-    local cols = view.format_entry_cols(file, {}, {}, test_adapter, false)
+    local cols = view.format_entry_cols(file, {}, {}, test_adapter)
     local lines = util.render_table({ cols }, {})
     table.insert(lines, "")
     table.insert(lines, "     ")
diff --git a/tests/preview_spec.lua b/tests/preview_spec.lua
deleted file mode 100644
index 08aba78..0000000
--- a/tests/preview_spec.lua
+++ /dev/null
@@ -1,41 +0,0 @@
-require("plenary.async").tests.add_to_env()
-local TmpDir = require("tests.tmpdir")
-local oil = require("oil")
-local test_util = require("tests.test_util")
-local util = require("oil.util")
-
-a.describe("oil preview", function()
-  local tmpdir
-  a.before_each(function()
-    tmpdir = TmpDir.new()
-  end)
-  a.after_each(function()
-    if tmpdir then
-      tmpdir:dispose()
-    end
-    test_util.reset_editor()
-  end)
-
-  a.it("opens preview window", function()
-    tmpdir:create({ "a.txt" })
-    test_util.oil_open(tmpdir.path)
-    a.wrap(oil.open_preview, 2)()
-    local preview_win = util.get_preview_win()
-    assert.not_nil(preview_win)
-    assert(preview_win)
-    local bufnr = vim.api.nvim_win_get_buf(preview_win)
-    local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
-    assert.are.same({ "a.txt" }, preview_lines)
-  end)
-
-  a.it("opens preview window when open(preview={})", function()
-    tmpdir:create({ "a.txt" })
-    test_util.oil_open(tmpdir.path, { preview = {} })
-    local preview_win = util.get_preview_win()
-    assert.not_nil(preview_win)
-    assert(preview_win)
-    local bufnr = vim.api.nvim_win_get_buf(preview_win)
-    local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
-    assert.are.same({ "a.txt" }, preview_lines)
-  end)
-end)
diff --git a/tests/select_spec.lua b/tests/select_spec.lua
index 9de2eb4..d4d3700 100644
--- a/tests/select_spec.lua
+++ b/tests/select_spec.lua
@@ -8,7 +8,8 @@ a.describe("oil select", function()
   end)
 
   a.it("opens file under cursor", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     -- Go to the bottom, so the cursor is not on a directory
     vim.cmd.normal({ args = { "G" } })
     a.wrap(oil.select, 2)()
@@ -17,7 +18,8 @@ a.describe("oil select", function()
   end)
 
   a.it("opens file in new tab", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     local tabpage = vim.api.nvim_get_current_tabpage()
     a.wrap(oil.select, 2)({ tab = true })
     assert.equals(2, #vim.api.nvim_list_tabpages())
@@ -26,7 +28,8 @@ a.describe("oil select", function()
   end)
 
   a.it("opens file in new split", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     local winid = vim.api.nvim_get_current_win()
     a.wrap(oil.select, 2)({ vertical = true })
     assert.equals(1, #vim.api.nvim_list_tabpages())
@@ -35,7 +38,8 @@ a.describe("oil select", function()
   end)
 
   a.it("opens multiple files in new tabs", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     vim.api.nvim_feedkeys("Vj", "x", true)
     local tabpage = vim.api.nvim_get_current_tabpage()
     a.wrap(oil.select, 2)({ tab = true })
@@ -45,7 +49,8 @@ a.describe("oil select", function()
   end)
 
   a.it("opens multiple files in new splits", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     vim.api.nvim_feedkeys("Vj", "x", true)
     local winid = vim.api.nvim_get_current_win()
     a.wrap(oil.select, 2)({ vertical = true })
@@ -58,7 +63,8 @@ a.describe("oil select", function()
     a.it("same window", function()
       vim.cmd.edit({ args = { "foo" } })
       local bufnr = vim.api.nvim_get_current_buf()
-      test_util.oil_open()
+      oil.open()
+      test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
       -- Go to the bottom, so the cursor is not on a directory
       vim.cmd.normal({ args = { "G" } })
       a.wrap(oil.select, 2)({ close = true })
@@ -73,7 +79,8 @@ a.describe("oil select", function()
       vim.cmd.edit({ args = { "foo" } })
       local bufnr = vim.api.nvim_get_current_buf()
       local winid = vim.api.nvim_get_current_win()
-      test_util.oil_open()
+      oil.open()
+      test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
       a.wrap(oil.select, 2)({ vertical = true, close = true })
       assert.equals(2, #vim.api.nvim_tabpage_list_wins(0))
       assert.equals(bufnr, vim.api.nvim_win_get_buf(winid))
@@ -83,7 +90,8 @@ a.describe("oil select", function()
       vim.cmd.edit({ args = { "foo" } })
       local bufnr = vim.api.nvim_get_current_buf()
       local tabpage = vim.api.nvim_get_current_tabpage()
-      test_util.oil_open()
+      oil.open()
+      test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
       a.wrap(oil.select, 2)({ tab = true, close = true })
       assert.equals(1, #vim.api.nvim_tabpage_list_wins(0))
       assert.equals(2, #vim.api.nvim_list_tabpages())
diff --git a/tests/test_util.lua b/tests/test_util.lua
index 46f63df..0076689 100644
--- a/tests/test_util.lua
+++ b/tests/test_util.lua
@@ -1,7 +1,6 @@
 require("plenary.async").tests.add_to_env()
 local cache = require("oil.cache")
 local test_adapter = require("oil.adapters.test")
-local util = require("oil.util")
 local M = {}
 
 M.reset_editor = function()
@@ -34,10 +33,6 @@ local function throwiferr(err, ...)
   end
 end
 
-M.oil_open = function(...)
-  a.wrap(require("oil").open, 3)(...)
-end
-
 M.await = function(fn, nargs, ...)
   return throwiferr(a.wrap(fn, nargs)(...))
 end
@@ -58,10 +53,6 @@ M.wait_for_autocmd = a.wrap(function(autocmd, cb)
   vim.api.nvim_create_autocmd(autocmd, opts)
 end, 2)
 
-M.wait_oil_ready = a.wrap(function(cb)
-  util.run_after_load(0, vim.schedule_wrap(cb))
-end, 1)
-
 ---@param actions string[]
 ---@param timestep integer
 M.feedkeys = function(actions, timestep)
diff --git a/tests/util_spec.lua b/tests/util_spec.lua
deleted file mode 100644
index 3193842..0000000
--- a/tests/util_spec.lua
+++ /dev/null
@@ -1,29 +0,0 @@
-local util = require("oil.util")
-describe("util", function()
-  it("url_escape", function()
-    local cases = {
-      { "foobar", "foobar" },
-      { "foo bar", "foo%20bar" },
-      { "/foo/bar", "%2Ffoo%2Fbar" },
-    }
-    for _, case in ipairs(cases) do
-      local input, expected = unpack(case)
-      local output = util.url_escape(input)
-      assert.equals(expected, output)
-    end
-  end)
-
-  it("url_unescape", function()
-    local cases = {
-      { "foobar", "foobar" },
-      { "foo%20bar", "foo bar" },
-      { "%2Ffoo%2Fbar", "/foo/bar" },
-      { "foo%%bar", "foo%%bar" },
-    }
-    for _, case in ipairs(cases) do
-      local input, expected = unpack(case)
-      local output = util.url_unescape(input)
-      assert.equals(expected, output)
-    end
-  end)
-end)
diff --git a/tests/win_options_spec.lua b/tests/win_options_spec.lua
index cb638a7..6dbde27 100644
--- a/tests/win_options_spec.lua
+++ b/tests/win_options_spec.lua
@@ -9,28 +9,32 @@ a.describe("window options", function()
 
   a.it("Restores window options on close", function()
     vim.cmd.edit({ args = { "README.md" } })
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     assert.equals("no", vim.o.signcolumn)
     oil.close()
     assert.equals("auto", vim.o.signcolumn)
   end)
 
   a.it("Restores window options on edit", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.edit({ args = { "README.md" } })
     assert.equals("auto", vim.o.signcolumn)
   end)
 
   a.it("Restores window options on split ", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.split({ args = { "README.md" } })
     assert.equals("auto", vim.o.signcolumn)
   end)
 
   a.it("Restores window options on split", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.split()
     vim.cmd.edit({ args = { "README.md" } })
@@ -38,14 +42,16 @@ a.describe("window options", function()
   end)
 
   a.it("Restores window options on tabnew ", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.tabnew({ args = { "README.md" } })
     assert.equals("auto", vim.o.signcolumn)
   end)
 
   a.it("Restores window options on tabnew", function()
-    test_util.oil_open()
+    oil.open()
+    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.tabnew()
     vim.cmd.edit({ args = { "README.md" } })