Compare commits

...

272 commits

Author SHA1 Message Date
Github Actions
f55b25e493 [docgen] Update docs
skip-checks: true
2026-01-17 05:01:19 +00:00
zeta-squared
7a09f0b000
fix: add open_float params to toggle_float (#716)
* feat: `toggle_float` now takes the same params as `open_float`

* docs: update `toggle_float` docs for `opts` and `cb` params

* fix: ensure cb is always called

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2026-01-16 21:01:02 -08:00
malewicz1337
6b59a6cf62
feat: add support for column text alignment (#711)
* feat: add support for column text alignment

* refactor(util): replace rpad with pad_align

* refactor(columns): whitespace handling in parse_col

* refactor: small changes

* doc: add align option to doc generation

* refactor: replace lpad with pad_align

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2026-01-13 21:28:16 -08:00
Daniel Kongsgaard
fbbb2a9872
doc: fix s3 column descriptions (#715) 2026-01-12 11:26:43 -08:00
Sebastian Oberhoff
d278dc40f9
fix: propagate errors in recursive_delete and recursive_copy (#712)
The `complete` callback checks `err` instead of `err2`, but `err` is
always nil inside the `elseif entries` branch. This silently ignores
child operation errors, causing misleading "directory not empty" failures.
2026-01-11 13:55:32 -08:00
Github Actions
43227c5a1c [docgen] Update docs
skip-checks: true
2026-01-11 21:53:37 +00:00
Ross
24055701b7
feat: add horizontal scrolling actions (#709)
* feat: add horizontal scrolling actions

* refactor(actions): remove unnecessary use of `nvim_replace_termcodes`

* lint: apply stylua

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2026-01-11 13:53:17 -08:00
Steven Arcangeli
81b8a91735 cleanup: remove deprecated trash_command 2026-01-01 00:22:58 -05:00
jake-stewart
78ed0cf7d9
fix: multicursor when opened with --preview (#701) 2025-12-31 12:50:39 -08:00
Dominic Della Valle
963c8d2c55
fix: handle empty LSP glob patterns (#702)
* fix: handle empty LSP glob patterns

* fix: use non-greedy pattern matching

* lint: fix shadowed variable

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2025-12-29 12:27:20 -08:00
Muhammad Imaduddin
634049414b
fix: open files under cwd with relative name (#693) 2025-12-29 10:15:58 -08:00
phanium
bbfa7cba85
fix: args.count of 0 is not used as size (#695) 2025-12-27 13:27:37 -08:00
jake-stewart
756dec855b
feat: support multicursor.nvim (#696)
* support multicursor.nvim

* lint: apply stylua

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2025-12-21 12:26:10 -08:00
Steven Arcangeli
09a4e4f460 ci: fix type error 2025-12-21 15:14:45 -05:00
Github Actions
3b249b7195 [docgen] Update docs
skip-checks: true
2025-12-20 20:03:26 +00:00
Sebastian Lyng Johansen
15a2b21eda
fix: use g~ instead of overriding the builtin ~ mapping (#694) 2025-12-20 12:03:07 -08:00
phanium
cbcb3f997f
fix: command modifiers for :Oil (#691)
* Support command mods `:belowright hor 10Oil`
* Fix `:tab Oil` only work on the first tab
2025-11-30 14:01:40 -08:00
Siggsy
b9ab05fe5a
feat: add OilEmpty highlight group (#689)
* Add OilEmpty highlight

* Add OilEmpty to doc
2025-11-30 13:42:00 -08:00
Github Actions
e5a1398790 [docgen] Update docs
skip-checks: true
2025-11-30 20:41:54 +00:00
Daniel Kongsgaard
e5bd931edb
feat: new adapter for S3 buckets (#677)
* Added s3 support

* Save work

* Various bug fixes

* Minor cleanup

* Minor bug fixes

* Fix typo

* Update following feedback + minor bug fix

* Fix CI

* Cleanup and remove bucket entry_type

* Make suggested changes

* Better aws existence check

* Fix typo

* refactor: don't bother caching aws executable status

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2025-11-30 12:41:37 -08:00
Zijian
01cb3a8ad7
fix: send_to_quickfix opens loclist when specified (#687)
* fix `send_to_quickfix` opening qf when the target is `loclist`

* fix indentation
2025-11-21 17:45:13 -08:00
Steven Arcangeli
7e1cd7703f fix: don't apply oil window options to non-directory oil buffers 2025-10-19 15:39:26 -07:00
Steven Arcangeli
71948729cd lint: use more specific type for internal entries 2025-10-15 10:42:52 -07:00
Steve Walker
f55ebb0079
feat(clipboard): pasting from system clipboard can delete original (cut) (#649)
* feat: cut_from_system_clipboard

* refactor: shuffle some code around

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2025-10-15 10:36:37 -07:00
Github Actions
64dbcaa91d [docgen] Update docs
skip-checks: true
2025-10-15 17:03:28 +00:00
John Winston
dfb09e87bf
feat: add callback for handling buffer opening (#638) 2025-10-15 10:03:09 -07:00
Sebastian Lyng Johansen
200df01e4b
fix: change default border config to nil (#643)
Neovim 0.11 introduced the winborder option, which serves the same purpose. By defaulting the border to nil, we will use whatever value the user has configured with winborder.

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2025-10-14 22:30:41 -07:00
Random Dude
919e155fdf
doc: mini.nvim has moved to new github organization (#663) 2025-09-27 08:48:27 -07:00
XeroOl
07f80ad645
fix: support natural ordering for numbers with >12 digits (#652)
* fix: support natural ordering for numbers with >12 digits

Changes the column ordering code when `view_options.natural_order`
is enabled, so that it can support larger numbers.

The previous 12-digit padding approach breaks for numbers above 12
digits.

This length-prefixed approach can scale to much higher numbers.
I picked %03 (padding 3 digits) because most filesystems don't allow
more than 255 bytes in a path segment, and "255" is 3 digits long.

* add memoization to natural order sorting

* remove call to unpack
2025-08-20 18:22:30 -07:00
Steven Arcangeli
bbad9a76b2 fix: scratch preview method (#628) 2025-07-02 09:18:28 -07:00
Ben O'Mahony
3b7c74798e
doc: add a mention to third party extension oil-git.nvim (#640) 2025-07-01 17:43:43 -07:00
jiz4oh
1498d2fccf
fix: ssh adapter supports iso8601 dates (#635)
* fix: add iso8601 format compatibility

* Update sshfs.lua

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2025-06-30 16:54:22 -07:00
kaerum
08c2bce8b0
fix: glob formatting on windows in neovim nightly (#631)
* fix: makes workaround conditional as it is no longer needed for 0.12

* fix: formatted with proper stylua version
2025-06-04 15:40:16 -07:00
Steven Arcangeli
5b6068aad7 fix: clean up empty buffer when opening in new tab (#616) 2025-06-01 11:02:23 -07:00
Steven Arcangeli
35f7f000f4 doc: add a mention for third-party extension plugins 2025-06-01 10:58:43 -07:00
Alexandros Alexiou
685cdb4ffa
fix: prevent E565 error when opening directories with nvim . (#608) 2025-04-20 10:35:57 -07:00
Steven Arcangeli
302bbaceea ci: run tests against nvim 0.11 2025-03-30 21:58:22 -07:00
Steven Arcangeli
5b38bfe279 doc: fix typecheck errors in nvim 0.11 2025-03-30 21:56:41 -07:00
Steven Arcangeli
ba1f50a9a8 fix: file time column escapes ()[] chars in parser (#603) 2025-03-30 15:14:11 -07:00
Shihua Zeng
ab887d926c
fix: indexing nil when env vars does not exist (#601) 2025-03-20 16:40:24 -07:00
Steve Walker
4c9bdf0d83
feat: copy/paste to system clipboard (#559)
* feat: copy/paste to system clipboard on macOS

* stylua

* feat: copy/paste to system clipboard on linux

* force mime type

* fix string.gsub

* vim.uv or vim.loop

* fix stylua

* support gnome directly

* support wayland

* refactor: extract clipboard actions into separate file

* fix: copy/paste in KDE

* refactor: simplify file loading

* fix: copy/paste on x11

* fix: better error message when clipboard command not found

* fix: paste on mac

* fix: pasting in Gnome

* feat: support pasting multiple files

* feat: support copying multiple files to clipboard

---------

Co-authored-by: Steve Walker <65963536+etherswangel@users.noreply.github.com>
Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2025-03-20 08:19:18 -07:00
Luis Calle
8649818fb2
fix(trash-win): don't hang when shellslash is enabled (#592) 2025-03-19 15:10:58 -07:00
Steven Arcangeli
548587d68b fix: better detection of oil buffers (#589) 2025-03-04 22:12:47 -08:00
skshetry
54fe7dca36
fix: pass bufnr to constrain_cursor (#574)
* pass bufnr to the constrain_cursor

* return early if the oil buffer is not the current buffer

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2025-03-04 16:44:26 -08:00
Steven Arcangeli
d7c61c7084 fix: silent handling when buffer has no oil adapter (#573) 2025-03-04 12:57:01 -08:00
github-actions[bot]
975a77cce3
chore(master): release 2.15.0 (#545)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-15 14:15:32 -08:00
forestchen1224
7cde5aab10
fix: disable_preview respected when preview_method != "load" (#577)
* fix bug of disable_preview

file should not loaded if disable_preview is true

* refeactor function open_preview about disable_preview

switch the condition checking `disable_preview` of `if`
move the longer condition to the `elseif`
swap their repective code blocks to maintain the same functionality

* refactor: simplify conditionals

* fix: missing then

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2025-02-13 14:22:54 -08:00
Steven Arcangeli
32dd3e378d feat: most moves and copies will copy the undofile (#583) 2025-02-13 09:40:01 -08:00
Steven Arcangeli
5313690956 fix: more robust parsing of custom column timestamp formats (#582) 2025-02-12 22:12:24 -08:00
Ian Wright
8abc58b038
feat: add support for bufnr in column rendering functions (#575)
This is primarily for user-defined custom columns, which may want access
to the current path or similar information
2025-02-12 16:49:43 -08:00
Steven Arcangeli
abbfbd0dbc lint: fix typecheck warning 2025-02-11 21:07:07 -08:00
Steven Arcangeli
20baf82747 ci: update nvim install script for new appimage name 2025-02-11 17:42:06 -08:00
Github Actions
add50252b5 [docgen] Update docs
skip-checks: true
2025-01-26 17:18:56 +00:00
Anton Janshagen
b594b9a905
feat: can selectively add entries to quickfix (#564)
* bugfix: fix to enable adding or replacing of quickfix entries

* feat: added option to send only matched files to the quickfix list
2025-01-26 09:18:37 -08:00
Steven Arcangeli
a3fc6623fa lint: upgrade to stylua v2.0.2 2025-01-24 22:54:50 -08:00
Steven Arcangeli
81b2c5f04a fix: crash in preview on nvim 0.8 2025-01-24 16:22:21 -08:00
Steven Arcangeli
6f9e1057c5 lint: rename shadowed variable 2025-01-24 16:00:44 -08:00
Steven Arcangeli
2f6ed70161 test: refactor tests to use new helper methods 2025-01-24 15:41:27 -08:00
Steven Arcangeli
57528bf9c5 feat: API to automatically open preview window after opening oil (#339) 2025-01-24 15:16:54 -08:00
Julian Visser
52f1683c76
doc: add note discouraging lazy loading (#565)
* Add disable lazy loading to lazy.nvim install

* doc: rephrase

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2025-01-24 10:51:18 -08:00
Peeranut Pongpakatien
83ac5185f7
fix: open files in correct window from floating oil (#560) 2025-01-23 22:08:06 -08:00
Steven Arcangeli
7a782c9a9c refactor: officially deprecated trash_command 2025-01-22 21:19:02 -08:00
Steven Arcangeli
1b180d5491 doc: rephrase the instructions to restore a trashed file 2025-01-22 21:18:35 -08:00
Steven Arcangeli
8615e7da20 cleanup: remove open({preview = true}) shim 2025-01-22 21:17:21 -08:00
Steven Arcangeli
1488f0d96b fix: preview sometimes causes oil buffers to be stuck in unloaded state (#563) 2025-01-22 16:53:10 -08:00
Steven Arcangeli
c80fa5c415 fix: more consistent cursor position when entering a new directory (#536) 2025-01-22 15:10:41 -08:00
Steven Arcangeli
62c5683c2e lint: fix typecheck errors 2025-01-22 08:54:47 -08:00
Steven Arcangeli
8d11a2abf3 fix: error when non-current oil buffer has validation errors (#561) 2025-01-22 08:26:28 -08:00
Steven Arcangeli
09fa1d22f5
fix: work around incorrect link detection on windows (#557)
* fix: work around incorrect link detection on windows

* fix: gracefully handle lstat error on windows
2025-01-13 10:22:59 -08:00
Steven Arcangeli
7c26a59ac0
fix: gracefully handle fs_stat failures (#558)
* fix: gracefully handle fs_stat failures

* fix: make log methods safe to call in luv callbacks

* fix: replace another vimscript call
2025-01-12 14:29:46 -08:00
Benedict Ozua
7041528bde
fix: support permissions checks on windows and virtual filesystems (#555)
* use access(2) over file permission checks to workaround systems that change expected file permission view

* cleanup: delete unused function

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2025-01-07 21:04:25 -08:00
abdennourzahaf
1df90faf92
feat: floating window max width/height can be percentages (#553) 2025-01-07 19:07:22 -08:00
Github Actions
6290ba1dc2 [docgen] Update docs
skip-checks: true
2025-01-08 02:56:23 +00:00
Ian Wright
f5c563a074
feat: pass oil bufnr to custom filename highlight function (#552)
This enables you to determine the full directory path, enabling e.g.,
HL groups for Git
2025-01-07 18:56:03 -08:00
Ian Wright
a6a4f48b14
fix: directory rendering with custom highlights (#551)
These would loose their trailing '/', making them unusable
2025-01-06 21:11:10 -08:00
abdennourzahaf
b082ad5eb9
test: update test script shebang to be compatible with NixOS (#550) 2025-01-06 21:05:03 -08:00
Steven Arcangeli
c12fad2d22 doc: update winbar recipe to be window-specific (#546) 2025-01-04 21:50:16 -08:00
Steven Arcangeli
254bc6635c fix: guard against nil metadata values (#548) 2025-01-04 12:52:26 -08:00
Steven Arcangeli
c6a39a69b2 fix: stat files if fs_readdir doesn't provide a type (#543) 2025-01-03 11:55:50 -08:00
Steven Arcangeli
1f7da07a3e refactor: remove overcomplicated meta_fields abstraction
This abstraction is overly generic for what it does. It's only ever used
to help us conditionally perform a fs_stat for the local files adapter.
We can replace that with a much dumber, much simpler bit of logic.
2025-01-03 11:55:50 -08:00
github-actions[bot]
ba858b6625
chore(master): release 2.14.0 (#515)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-21 13:05:01 -05:00
David Marchante
c5f7c56644
fix: set alternate when using floating windows (#526) 2024-12-20 23:18:49 -05:00
Gustavo Sampaio
78ab7ca107
fix: don't take over the preview window until it's opened for oil (#532) 2024-12-20 23:15:47 -05:00
Steven Arcangeli
dba0375988 fix: handle files with newlines in the name (#534) 2024-12-10 15:22:24 -08:00
lucascool12
7a55ede5e7
fix: improper file name escaping (#530) 2024-12-10 08:08:43 -08:00
Github Actions
9a59256c8e [docgen] Update docs
skip-checks: true
2024-12-03 17:45:14 +00:00
Steven Arcangeli
f2b324933f feat: better merging of action desc when overriding keymaps 2024-12-03 09:44:07 -08:00
Steven Arcangeli
3c2de37acc debug: include shell command in error message 2024-11-25 09:10:33 -08:00
Steven Arcangeli
da93d55e32 fix: work around performance issue with treesitter, folds, and large directories 2024-11-24 15:04:00 -08:00
Foo-x
99ce32f4a2
feat: config option to customize filename highlight group (#508)
* feat: highlight config

Refs #402

* perf: minimize perf impact when option not provided

* doc: regenerate documentation

* fix: symbolic link rendering

* refactor: simplify conditional

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-11-22 10:23:08 -08:00
Ezekiel Warren
60e68967e5
feat: highlight groups for hidden files (#459)
* feat: hidden highlights

* feat: OilHidden for hidden highlights instead of Comment

* fix: add the new combinatoric highlight groups

* perf: get rid of a call to is_hidden_file

* fix: tweak the default highlight group links

* fix: update function call in unit tests

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-11-22 08:55:55 -08:00
Foo-x
740b8fd425
feat: add highlight group for orphaned links (#502)
* feat: add highlight for orphan links

Closes #501

* feat: add OilOrphanLinkTarget highlight group

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-11-22 08:17:50 -08:00
Steven Arcangeli
5fa528f552 chore: refactor benchmarking to use benchmark.nvim 2024-11-21 21:39:05 -08:00
Steven Arcangeli
3fa3161aa9 feat: config option to disable previewing a file 2024-11-21 17:36:40 -08:00
Steven Arcangeli
5acab3d8a9 fix: image.nvim previews with preview_method=scratch 2024-11-21 17:36:22 -08:00
Github Actions
bf81e2a79a [docgen] Update docs
skip-checks: true
2024-11-21 05:06:28 +00:00
cdmill
81cc9c3f62
feat: option to quite vim if oil is closed as last buffer (#491)
* feat: auto-quit vim if oil is closed as last buffer

* rename auto_close_vim to auto_close_last_buffer

* rework actions.close to be more like actions.cd

* fix: configure close action correctly

* add type annotation, future proofing

* fix: typo

* fix: typo

* refactor: better type annotations and backwards compatibility

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2024-11-20 21:06:09 -08:00
Jalal El Mansouri
21705a1deb
feat: use scratch buffer for file previews (#467)
* Initial implementation of scratch based preview

* Fix call to buf is valid in loop

* Fixing call to be made only from the main event loop

* Improve handling of large files from @pkazmier

* Better error handling and simplifying the code

* Default to old behavior

* Add documentation

* Fix readfile

* Fix the configuration

* refactor: single config enum and load real buffer on BufEnter

* doc: regenerate documentation

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-11-19 17:24:24 -08:00
Steven Arcangeli
8ea40b5506 fix: cursor sometimes does not hover previous file 2024-11-14 22:21:11 -08:00
Steven Arcangeli
651299a6ca doc: trashing on windows works now 2024-11-14 19:47:50 -08:00
Steven Arcangeli
c96f93d894 perf: optimize rendering cadence 2024-11-14 19:29:22 -08:00
Steven Arcangeli
792f0db6ba perf: only sort entries after we have them all 2024-11-14 19:29:22 -08:00
Steven Arcangeli
4de30256c3 perf: replace vim.endswith and vim.startswith with string.match 2024-11-14 19:29:22 -08:00
Steven Arcangeli
01b0b9d8ef perf: change default view_options.natural_order behavior to disable on large directories 2024-11-14 19:29:22 -08:00
Steven Arcangeli
7d4e62942f test: add harness for measuring performance 2024-11-14 19:29:21 -08:00
Steven Arcangeli
0472d9296a lint: fix typechecking for new LuaLS version 2024-11-13 08:58:16 -08:00
Github Actions
8735d185b3 [docgen] Update docs
skip-checks: true
2024-11-12 18:38:53 +00:00
Micah Halter
bbeed86bde
feat: add win_options to preview_win (#514) 2024-11-12 10:38:35 -08:00
Steve Walker
c23fe08e05
feat: disable preview for large files (#511)
* feat: disable preview for large files

fix: update oil.PreviewWindowConfig

* refactor: remove unnecessary shim in config.lua

* refactor: revert changes to shim

---------

Co-authored-by: Steve Walker <65963536+etherswangel@users.noreply.github.com>
Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2024-11-12 08:24:39 -08:00
github-actions[bot]
50c4bd4ee2
chore(master): release 2.13.0 (#478)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-10 19:18:46 -08:00
Github Actions
6e754e6699 [docgen] Update docs
skip-checks: true
2024-11-11 00:40:33 +00:00
Steven Arcangeli
3499e26ef4 chore: rework Makefile to not depend on direnv 2024-11-10 16:38:45 -08:00
Steven Arcangeli
2f5d4353ee doc: improve type annotations for oil.open_preview 2024-11-10 16:07:29 -08:00
Steven Arcangeli
eb5497f0ac refactor: rename 'preview' config to 'preview_win' 2024-11-10 15:57:31 -08:00
Steven Arcangeli
1f5b002270 refactor: rename action preview window to 'confirmation' window 2024-11-10 15:57:27 -08:00
Steven Arcangeli
621f8ba4fa fix: guard against nil keymaps 2024-11-09 22:31:35 -08:00
Steven Arcangeli
709403ccd6 fix: don't deep merge keymaps (#510) 2024-11-09 22:28:24 -08:00
Foo-x
52cc8a1fb3
fix: sort keymap help entries by description (#506)
Closes #376
2024-10-30 08:53:36 -07:00
Foo-x
42333bb46e
fix: add trailing slash to directories on yank_entry (#504)
* feat: add trailing slash on yank_entry

Closes #503

* style: format
2024-10-28 10:55:25 -07:00
Yu Guo
cca1631d5e
fix: actions.preview accepts options (#497)
* fix: pass opts to actions.preview

* add opts type to action.preview

* run generate.py script
2024-10-25 09:08:39 -07:00
Yu Guo
28aca0c1f5
chore: add __pycache__ to gitignore (#498)
* add __pycache__ to gitignore

* doc: fix typo

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2024-10-25 09:07:11 -07:00
Github Actions
39dbf87586 [docgen] Update docs
skip-checks: true
2024-10-16 02:22:43 +00:00
Philipp Oeschger
5d2dfae655
feat: config option to customize floating window title (#482)
* replace cwd path in actual path

* move get_title to utils

* add documentation

* rename

* add method doc

* add comment

* fallback to 0 for winid

* add missing property definition for relative_win_title

* only replace when at the start of the path

* simplify

* minor change

* add entry point to customize floating window title for oil-buffer

* remove config parameter

* cleanup

* add documentation

* move get_win_title to top and pass winid as parameter

* add get_win_title to type definition for oil.setup

* remove empty line

* adjust comment

---------

Co-authored-by: Philipp Oeschger <philippoeschger@Philipps-Air.fritz.box>
2024-10-15 22:22:31 -04:00
staticssleever668
ccab9d5e09
fix: only map ~ for normal mode (#484)
Allows to switch character case with ~ (tilde) in visual mode while
preserving existing ~ :tcd functionality.
Related to [1].

[1]: https://github.com/stevearc/oil.nvim/issues/397 "bug: ~ not respected"
2024-10-03 20:51:55 -07:00
Éric NICOLAS
9e6fb844fe
doc: Update links to FreeDesktop's Trash spec (#490)
Keeping it pinned to 1.0

Fixes #489
2024-09-30 22:11:09 -07:00
Steven Arcangeli
581c729805 doc: disable some type warnings from new LuaLS release 2024-09-30 22:10:36 -07:00
Steven Arcangeli
1360be5fda lint: stricter type checking 2024-09-17 13:00:48 -07:00
csponge
f60bb7f793
feat: config option to disable lsp file methods (#477)
* added config option to enable or disable lsp_file_methods

* refactor: rename enable -> enabled

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-09-11 19:41:46 -07:00
Github Actions
665bf2edc9 [docgen] Update docs
skip-checks: true
2024-09-11 15:45:10 +00:00
Steven Arcangeli
eadc3ed42e doc: add recipe to show current directory in the winbar 2024-09-11 08:44:45 -07:00
github-actions[bot]
1eb9fb35a4
chore(master): release 2.12.2 (#472)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-09-10 12:03:08 -07:00
Steven Arcangeli
b05374428e fix: wrap git rm callback in schedule_wrap (#475) 2024-09-10 11:44:15 -07:00
Steven Arcangeli
1fe476daf0 doc: more and better type annotations 2024-08-30 17:50:09 -07:00
Steven Arcangeli
d10e7f442f doc: fix recipe for hiding gitignored files 2024-08-30 17:10:53 -07:00
Github Actions
0dc98d36b5 [docgen] Update docs
skip-checks: true
2024-08-29 16:26:36 +00:00
Micah Halter
85637c1e63
doc(recipes): improve git integrated hidden files recipe (#470) 2024-08-29 09:26:19 -07:00
csponge
30e0438ff0
fix: ensure win_options are being set on correct window (#469)
* Added check for filetype before setting win_options in initialize

* refactor: use nvim_buf_call to set window options

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2024-08-28 17:54:03 -07:00
Micah Halter
0fcd1263a2
perf(view): avoid running is_hidden_file when show_hidden is set (#471) 2024-08-28 17:13:57 -07:00
github-actions[bot]
4f3c6780ff
chore(master): release 2.12.1 (#468)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-25 21:03:15 -07:00
Steven Arcangeli
70337eb77f fix: gracefully handle trashing file that does not exist 2024-08-25 20:51:27 -07:00
Steven Arcangeli
349bca8c3e fix: process deletes in dir before moving dir 2024-08-25 20:46:46 -07:00
github-actions[bot]
a632c898fb
chore(master): release 2.12.0 (#434)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-16 21:37:00 -07:00
Julian
b39a78959f
fix: add compatibility for Lua 5.1 (#456)
Some architectures don't support LuaJIT.
Remove the goto's from the code to be compatible
with Neovim built without LuaJIT.

Signed-off-by: Julian Ruess <julianonline+github@posteo.de>
2024-08-16 21:33:59 -07:00
Steven Arcangeli
fcca212c2e fix: handle rare case where file watcher outlives buffer 2024-07-29 17:12:25 -07:00
Anna Arad
71c972fbd2
fix: Force standard C locale when getting ls input for parsing in SSH (#455) 2024-07-22 21:17:38 -07:00
Anna Arad
a6cea1a5b9
fix: Handle users and groups with spaces over SSH (#448) 2024-07-21 16:07:10 -07:00
Steven Arcangeli
9e5eb2fcd1 doc: make lazy.nvim snippet more copy/paste-able (#445) 2024-07-15 14:01:56 -07:00
Github Actions
10fbfdd37b [docgen] Update docs
skip-checks: true
2024-07-06 23:24:46 +00:00
sleeptightAnsiC
cc2332599f
feat: allow bufnr optional parameter for get_current_dir function (#440)
This allows for using get_current_dir in cases where currently hovered
buffer is not the desired Oil buffer (e.g. displaying directories for
multiple different Oil buffers)
2024-07-06 16:24:33 -07:00
Micah Halter
d5e56574f8
fix: correctly check if mini.icons is actually setup (#441)
This leaves the `pcall` just so (1) we load the plugin if it is lazy
loaded by the user and (2) we get LSP completion/validation with that
type as well.
2024-07-06 16:20:44 -07:00
Micah Halter
a543ea598e
feat: add support for mini.icons (#439) 2024-07-05 15:13:10 -07:00
Steven Arcangeli
b5a1abfde0 fix: cursor sometimes disappears after making changes (#438) 2024-07-03 18:14:52 -07:00
icefed
b15e4c1e64
feat: disable cursor in preview window (#433) 2024-07-02 10:31:26 -07:00
icefed
b0a6cf9898
fix: set floating window win_options when buffer changes (#432)
* fix: set floating window win_options when buffer changes

* fix: set win options even when float border is "none"

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-07-02 10:03:08 -07:00
github-actions[bot]
ace46a41a1
chore(master): release 2.11.0 (#427)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-01 11:51:47 -07:00
Philipp Oeschger
2077cc3358
feat: case insensitive sorting (#429)
* check for sorting option in netrw

* documentation

* refactor: remove sort_ prefix

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-07-01 11:41:04 -07:00
Steven Arcangeli
c7c7ce5bd4 feat: rename experimental_watch_for_changes -> watch_for_changes
I've been using this for a while now and haven't seen any issues. We can
take "experimental" out of the name.
2024-07-01 11:34:04 -07:00
DerpDays
65c53dbe4f
fix: correctly check group permissions in unix (#428)
* fix: set modifiable when user in group

* feat: add mode caching, fallback to previous, and better checking of permissions

* fix: make is_modifiable check group permissions even if the user is owner of the directory

* refactor: simplify group ID caching

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-06-21 09:07:03 -04:00
Steven Arcangeli
f6df58ad37 fix: bug in buffer rendering race condition handling 2024-06-20 22:45:28 -04:00
Steven Arcangeli
4c574cf4a2 fix: increase loading display delay to avoid flicker (#424) 2024-06-19 21:41:05 -04:00
Philipp Oeschger
59b3dab6f7
feat: support preview from floating window (#403)
* implement floating window

* reset width on closing window

* use gap from new config parameter

* use minimal style for preview in floating

* lower z-index

* add configuration of preview position in floating window

* fix in verions earlier than nvim 0.10

* close preview on opening floating window

Close the any existing preview because otherwise strange errors happen when the preview is open and the floating window is opened at the same time.

* reset formatting changes

* remove empty line

* change z-index of preview window to floating window z-index

* add configurations to oil.txt

* formatting

* add auto configuration

* update oil doc

* refactor: move logic into layout.lua and eliminate flicker

* fix: floating preview window title is file name

* doc: clarify default_file_explorer

* refactor: don't need a preview_gap option

* refactor: only find preview win in current tabpage

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-06-19 21:23:30 -04:00
github-actions[bot]
0883b109a7
chore(master): release 2.10.0 (#379)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-06-19 21:21:43 -04:00
HyBer
64a3a555b4
doc: update <C-t> desc (#419) 2024-06-15 22:18:59 -04:00
Steven Arcangeli
b77ed915ab doc: add recipes 2024-06-13 18:05:32 -04:00
Steven Arcangeli
ca8b62fca5 doc: restore description for keymaps 2024-06-13 18:05:30 -04:00
k14lb3
c82b26eb4b
fix: incorrect default config actions (#414) 2024-06-12 11:17:42 -04:00
Steven Arcangeli
76bfc25520 fix: vim.notify call error 2024-06-11 06:27:39 -05:00
Nam Nguyen
61f1967222
fix: throw error on vim.has call within the lsp/workspace.lua (#411) 2024-06-11 06:23:04 -05:00
Steven Arcangeli
e5eb20e88f fix: change unknown action name from error to notification 2024-06-11 06:21:33 -05:00
Steven Arcangeli
a62ec258d1 refactor: Neovim 0.11 won't need the glob ordering hack 2024-06-10 16:45:03 -05:00
Steven Arcangeli
96368e13e9 feat: keymap actions can be parameterized 2024-06-10 16:44:59 -05:00
Ruslan Hrabovyi
18272aba9d
fix: notify when changing the current directory (#406)
* feat: notify when changing the current directory

* Update actions.lua

---------

Co-authored-by: Ruslan Hrabovyi <ruslan.hrabovyi@ligadigital.com>
Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2024-06-08 23:35:32 -04:00
Steven Arcangeli
e5312c3a80 fix: hack around glob issues in LSP rename operations (#386) 2024-06-05 20:33:44 -07:00
Kevin Traver
bbc0e67eeb
feat: add copy filename action (#391) 2024-06-03 11:02:01 -07:00
Steven Arcangeli
d3a365c950 doc: improve documentation for set_sort (#401) 2024-06-02 17:45:22 -07:00
Steven Arcangeli
15e071f203 ci: typechecking no longer requires neodev 2024-06-01 16:38:11 -07:00
csponge
8ac4ba4e0a
Return from delete_hidden_buffers when win type is command (#394)
* Return from delete_hidden_buffers when win type is command

* lint: apply stylua formatting

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-06-01 12:39:14 -07:00
Steven Arcangeli
2cb39e838e doc: more detailed type annotations for setup() call 2024-05-22 13:46:11 -07:00
Steven Arcangeli
259b1fbc84 doc: better type annotations for API methods 2024-05-21 19:39:37 -07:00
Steven Arcangeli
06a19f77f1 fix: error opening command window from oil float (#378) 2024-05-17 12:02:35 -07:00
Steven Arcangeli
9e3a02252d ci: remove package-name from release-please config 2024-05-16 12:24:19 -07:00
Steven Arcangeli
6f452e8d47 ci: upgrade release-please-action to v4 2024-05-16 12:16:00 -07:00
github-actions[bot]
80eb2d6719
chore(master): release 2.9.0 (#354)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-05-16 11:32:12 -07:00
Steven Arcangeli
27d9f37161 ci: run tests against Neovim v0.10.0 2024-05-16 11:09:15 -07:00
Steven Arcangeli
8a2de6ada2 ci: update stylua version to v0.20.0 2024-05-16 10:46:45 -07:00
Steven Arcangeli
f630887cd8
fix(windows): convert posix paths before matching LSP watch globs (#374) 2024-05-14 22:55:18 -06:00
Steven Arcangeli
3283deec96 lint: ignore some type errors 2024-05-13 20:02:11 -06:00
Steven Arcangeli
aa0c00c7fd fix(ssh): bad argument when editing files over ssh (#370) 2024-05-13 11:35:50 -06:00
pn-watin
010b44a79d
refactor: preview window uses Yes/No instead of Ok/Cancel (#344)
NOTE: the `o` and `c` keymaps will continue to work. This only changes
the text labels and adds new keymaps for `y` and `n`.

* chore: replace ok and cancel with yes and no in confirmation window

* chore: allow to configure labels and keymaps for confirmation window

* chore: remove potential duplicate cancel keymaps

* chore: update README and oil.txt

* chore: nowait on confirm mappings and cleanup

* refactor: fully transition to yes/no

* move the config from under the `confirmation` key to the `preview`
  key, which is already in use for customizing the window
* fully default to yes/no, keeping the o/c keybindings for backwards
  compatibility
* make all of the `cancel` keybindings explicit (q, C-c, esc)
* more dynamically choose highlighting of the action labels

* refactor: just use yes/no and abandon configuration

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-05-06 12:03:30 -07:00
Oleg Kovalev
752563c59d
fix: icon column highlight parameter (#366)
* fix: icon column highlight

* fix: support icon highlight function

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-05-06 11:43:00 -07:00
ericguin
3abb6077d7
fix(ssh): config option to pass extra args to SCP (#340)
* Adding in SCP options configuration

This changeset adds in additional SCP options to the config. This allows
the user to specify a list of flags to send to the SCP command that will
be expanded into each shell command.

The primary driver for this is from newe boxes SSHing into pre 9 openSSH
boxes. New openSSH uses sftp server under the hood, rather than the
older SCP protocol. If you go into a system that does not have these
changes, SCP fails to work. The '-O' command line flag was introduced to
resolve this.

Using this change, the user can now pass in `extra_scp_options = {"-O"}`
to resolve the issue.

* Replacing table.unpack with global unpack

* lint: apply stylua

* refactor: change option name and shuffle config around

---------

Co-authored-by: Eric Guinn <eric_guinn@selinc.com>
Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-05-06 10:42:23 -07:00
Steven Arcangeli
bcfc0a2e01 fix(ssh): garbled output when directory has broken symlinks
The stderr was interleaving with the stdout when performing one of the
ls operations. This was causing the parsing to sometimes fail and crash.
2024-05-01 16:10:10 -07:00
Matthew Wilding
f3a31eba24
fix(windows): navigating into drive letter root directories (#341)
* Fixed drive browsing on windows

* Fixed naming

* fix: Uppercase drive letter only

* updated: Filter out network drives on windows

* Update files.lua

* Update files.lua

* fixed: mapped drives

* addslash to check for double slash

* Fixed indents

* Reverted addslash change

* Fixed windows initial buffer name

* Reverted formatting

* Cleaned up callback

* Fix addslash to handle \ too

* Allow running tests workflow from fork

* Fix workflow

* Test

* Tests

* refactor: readability and comments

* fix: convert buffer name to posix when hijacking directory

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-04-23 22:06:59 -07:00
Steven Arcangeli
3b3a6b23a1 fix(windows): treat both backslash and frontslash as path separators (#336) 2024-04-23 21:04:31 -07:00
Steven Arcangeli
96f0983e75 fix(windows): file operation preview uses only backslash path separator (#336) 2024-04-23 20:57:22 -07:00
Steven Arcangeli
be0a1ecbf0 fix: gracefully handle new dirs with trailing backslash on windows (#336) 2024-04-23 20:57:14 -07:00
Steven Arcangeli
6a7a10b611 fix: git mv errors when moving empty directory (#358) 2024-04-23 19:31:54 -07:00
Steven Arcangeli
2edb43a7ec ci: update checkout action 2024-04-23 19:21:02 -07:00
Steven Arcangeli
f41a0f24c0 lint: remove unused variable 2024-04-21 07:55:19 -07:00
Steven Arcangeli
a3c03e442a test: add regression test for #355 2024-04-21 07:48:03 -07:00
Steven Arcangeli
2bc56ad68a fix: error when opening files from floating oil window (#355) 2024-04-21 07:37:54 -07:00
Steven Arcangeli
1f05774e1c feat: experimental support for git operations (#290) 2024-04-19 18:00:44 -04:00
Steven Arcangeli
354c53080a fix: duplicate create actions (#334) 2024-04-19 17:05:19 -04:00
Steven Arcangeli
c86e48407b fix: race condition when entering oil buffer (#321) 2024-04-19 16:31:42 -04:00
Steven Arcangeli
f41d7e7cd8 fix: support visual mode when preview window is open (#315) 2024-04-19 16:31:42 -04:00
Steven Arcangeli
fa3820ebf1 feat: can restore Oil progress window when minimized 2024-04-19 16:31:42 -04:00
github-actions[bot]
78aeb665e2
chore(master): release 2.8.0 (#331)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-19 10:46:24 -04:00
Steven Arcangeli
2bd71dda88 lint: fix typechecking 2024-04-19 10:44:16 -04:00
Steven Arcangeli
6c48ac7dc6 fix: output suppressed when opening files (#348) 2024-04-19 10:40:35 -04:00
Kevin Oberlies
8bb35eb81a
fix(ssh): escape all file paths for the ssh adapter (#353)
* Escape all paths for ssh file changes using `vim.fn.shellescape()`

* Change away from `vim.fn.shellescape` to custom implementation

Really, only escape `'` with `'\\''` so that it will:
- exit the single quote mode
- escape out a single quote character
- and get back into the single quote mode

Also format long line so linter doesn't complain

* Adding doc comments to the shellescape function

* Adding actual words to the doc comment
2024-04-17 16:19:10 -04:00
Steven Arcangeli
e462a34465 feat: add user autocmds before and after performing actions (#310) 2024-03-17 19:50:31 -07:00
github-actions[bot]
32e18df30f
chore(master): release 2.7.0 (#286)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-12 20:50:38 -07:00
Steven Arcangeli
0de8e60e3d fix: correctly reset bufhidden for formerly previewed buffers (#291) 2024-03-12 20:41:23 -07:00
Steven Arcangeli
17d71eb3d8 fix: window options sometimes not set in oil buffer (#287) 2024-03-12 20:06:02 -07:00
Steven Arcangeli
f259347d4d cleanup: remove old unused logic for transferring window variables 2024-03-12 19:37:57 -07:00
Github Actions
8af4afabb3 [docgen] Update docs
skip-checks: true
2024-03-13 02:22:11 +00:00
Lucas Eras Paiva
71b076b3af
feat: use natural sort order by default (#328)
* Sort entries with natural sorting

* refactor: move natural ordering logic and add config option

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-03-12 19:21:54 -07:00
Steven Arcangeli
e045ee3b4e fix: spurious exits from faulty :wq detection (#221)
The previous mechanism used histget() to get the last command or
expression to detect if the user issued a `:wq` or similar. This had the
issue where if a user issued a `:wq`, started vim again, then entered
oil and saved via some mechanism that is _not_ a command (e.g. a
keymap), we would incorrectly detect that `:wq` and exit after saving.
The new mechanism tracks all keypresses and may end up with false
negatives (e.g. ":wqaff<backspace><backspace>ll"), but those are less
frustrating than false positives.
2024-03-12 15:57:57 -07:00
Luis Calle
18dfd2458d
fix(windows): can delete non-ascii filenames to trash (#323) 2024-03-04 10:50:22 -08:00
Steven Arcangeli
c437f3c5b0 fix: potential leak in experimental file watcher 2024-03-02 18:56:18 -08:00
TheNordicMule
29a06fcc90
feat: add ability to alter lsp file operation timeout (#317)
* feat: add ability to alter lsp file operation timeout

* change default

* fix table

* add missing

* move inside table

* remove duplicate

* reuse default

* change message

* refactor: rename autosave config option

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-03-02 09:02:42 -08:00
Lucas Eras Paiva
132b4ea074
fix: close preview window when leaving oil buffer (#296)
* fix: close preview window when leaving oil buffer

* refactor: try different approach for closing the preview window

* fix: use util.is_oil_bufnr

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-02-22 22:11:32 -08:00
Steven Arcangeli
6953c2c17d fix: actions.open_external uses explorer.exe in WSL (#273) 2024-02-20 17:30:56 -08:00
Steven Arcangeli
bcfe7d1ec5 feat: experimental option to watch directory for changes (#292) 2024-02-19 23:27:27 -08:00
Reinder van Bochove
e27cc4e138
feat: add border config for SSH and keymaps help window (#299)
* feat: add config for ssh window border

* chore: add documentation for ssh window border

* feat: add config for keymaps help window border

* chore: add documentatoin for keymaps help window border
2024-02-19 20:00:49 -08:00
Lucas Eras Paiva
bf753c3e3f
feat: do not close preview when switching dirs (#277)
* feat: do not close preview when cd into dir

* refactor: add helper method to run function after oil buffer loads

* Keep preview window open

* Remove some test logic

* Use `run_after_load` when moving to parent directory

* Remove unnecessary update of current window

* refactor: create helper function for updating preview

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-01-21 20:32:02 -08:00
Steven Arcangeli
f0315c101f doc: document some configuration options (#283) 2024-01-21 09:34:44 -08:00
Steven Arcangeli
0ef49e495e doc: windows recycle bin is now supported 2024-01-21 08:59:19 -08:00
github-actions[bot]
dd432e76d0
chore(master): release 2.6.1 (#274)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-15 22:01:23 -08:00
Steven Arcangeli
ec24334471 fix(lsp_rename): handle absolute path glob filters (#279) 2024-01-16 05:55:11 +00:00
TheLeoP
e71b6caa95
perf(windows): use a single powershell process for trash operations (#271)
* perf(trash_windows): use a single powershell instance for operations

* refactor(trash_windows): encapsulate powershell connection logic

* refactor(windows_trash): better name for functions

* fix(windows_trash): set connection error on initializatino if needed

* refactor(windows_trash): simplify initialization code

* refactor: extract some powershell logic into separate file

---------

Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
2024-01-15 17:22:11 -08:00
Steven Arcangeli
8bc37bac29 ci: github workflow automation 2024-01-14 12:47:34 -08:00
Steven Arcangeli
a1af7a1b59 fix: diagnostic float would not open if scope=cursor (#275) 2024-01-14 12:35:35 -08:00
Steven Arcangeli
c4cc8240f1 fix: crash when LSP client workspace_folders is nil (#269) 2024-01-07 12:13:37 -08:00
Steven Arcangeli
49b2b3f4a5 fix(trash): mac error deleting dangling symbolic links to trash (#251) 2024-01-07 12:13:37 -08:00
github-actions[bot]
a128e6f75c
chore(master): release 2.6.0 (#267)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-02 22:07:24 -08:00
TheLeoP
553b7a0ac1
feat(trash): support for deleting to windows recycle bin (#243)
* feat(windows-trash): support for deleting to windows trash

* feat(windows-trash): add support for view, restore and purge

* fix(windows-trash): undefined path on M.list

* chore(windows-trash): modify comments

* fix(windows-trash): show correct original_path

* fix(windows-trash): add self to powershell_date_grammar

* fix(windows-trash-support): parse deleted date as number

* fix(fs): do not add innecesary \\ on Windows

* feat: extend windows trash adapter

* perf(windows-trash): powershell -> libuv (move, purge and copy)

* fix: don't prompt to save when opening trashed file

* lint: fix luacheck error

* lint: fix luacheck errors

* lint: luacheck error

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2024-01-02 22:05:01 -08:00
github-actions[bot]
523b61430c
chore(master): release 2.5.0 (#242)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-26 10:07:45 -08:00
Steven Arcangeli
5d9e4368d4 fix(trash): error deleting dangling symbolic links to trash (#251) 2023-12-26 18:03:00 +00:00
Steven Arcangeli
22ab2ce1d5 fix: handle opening oil from buffers with foreign schemes (#256) 2023-12-24 11:32:26 -05:00
Steven Arcangeli
71b1ef5edf feat: constrain_cursor option (closes #257) 2023-12-23 19:16:53 -05:00
Steven Arcangeli
a60c6d10fd fix: constrain cursor when entering insert mode
The main use case for this is hitting `I` from normal mode will now put
the cursor in insert mode at the beginning of the first editable column.
2023-12-23 19:02:09 -05:00
Steven Arcangeli
250e0af7a5
feat: support all LSP workspace file operations (#264) 2023-12-23 14:08:11 -08:00
Jack Tubbenhauer
48d8ea8f4a
feat: option to auto-save files affected by will_rename_files (#218)
* save buffers changed by will_rename_files

* prevent closing already open buffers

* chore: move to config option

* chore: fixes

* fix: a crash and some formatting

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2023-12-23 08:00:25 -08:00
Luckas
24027ed8d7
fix: willRename source path (#248)
* fix: willRename source path

* update: path matching handling

* lint: apply stylua

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2023-12-10 11:41:12 -08:00
Github Actions
1fce168881 [docgen] Update docs
skip-checks: true
2023-12-10 03:02:19 +00:00
umlx5h
ea612fe926
feat: add 'update_on_cursor_moved' option to preview window (#250) 2023-12-09 19:02:04 -08:00
Shihua Zeng
a173b5776c
feat: allow multiple hlgroups inside one column (#240)
* feat: allow multiple hlgroups inside one column

* types: refactor formatting of highlight types

* types: LuaLS can't infer type information from unpack

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
2023-12-07 21:36:52 -08:00
Github Actions
cd0c2d1f0a [docgen] Update docs
skip-checks: true
2023-12-08 05:01:27 +00:00
Luka Potočnik
3ffb8309e6
feat: actions for sending oil entries to quickfix (#249)
* Implement actions for sending oil entries to quickfix/location-list

* Use vim.notify instead of vim.notify_once in util quickfix function

* Remove redundant files/directories options for sending to qf

always send just files

* set qflist/loclist with a single call

* Add type annotations and default values for send_to_quickfix

* In visual mode, send only selected items to qf
2023-12-07 21:01:13 -08:00
Micah Halter
b3c24f4b3b
perf: speed up session loading (#246)
* perf: only execute on current buffer since this event is called on each buffer

* fix: only execute `SessionLoadPost` autocommand when a full session is loaded
2023-12-06 23:58:53 -08:00
Steven Arcangeli
2c80182d75
chore: add severity to feature request template 2023-12-05 17:06:12 -08:00
Steven Arcangeli
6782e2b64d
chore: add severity to bug report template 2023-12-05 17:03:14 -08:00
Yi Ming
82834573bb
feat: refresh action also clears search highlight (#228) 2023-12-03 17:39:25 -08:00
github-actions[bot]
58340545c6
chore(master): release 2.4.1 (#231)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-30 23:57:20 -08:00
Steven Arcangeli
636989b603 fix: oil.select respects splitbelow and splitright (#233) 2023-11-30 23:55:43 -08:00
Steven Arcangeli
e89a8f8ade fix: crash in ssh and trash adapter detail columns (#235) 2023-11-24 14:13:56 -08:00
Steven Arcangeli
05cb8257cb fix: bug copying file multiple times 2023-11-19 23:58:40 -08:00
Steven Arcangeli
8f0bf3789f lint: fix luacheck warnings 2023-11-19 21:13:47 -08:00
Steven Arcangeli
303f31895e fix: buffer data cleared when setting buflisted = false
For posterity: this was a very painful lesson. Turns out "BufDelete"
doesn't mean "buffer was deleted", it means "buffer was deleted from the
buffer list". If you set nobuflisted, BufDelete will be triggered even
though the buffer is still loaded and active.
2023-11-19 21:10:41 -08:00
Steven Arcangeli
6566f457e4 fix: preserve buflisted when re-opening oil buffers (#220) 2023-11-19 21:02:06 -08:00
Steven Arcangeli
4df43ad5f5 test: fix flaky tests 2023-11-19 21:02:06 -08:00
github-actions[bot]
af04969c43
chore(master): release 2.4.0 (#216)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-14 22:49:58 -08:00
Steven Arcangeli
2baf36d74f ci: run tests against nvim 0.9.4 2023-11-14 22:47:30 -08:00
Steven Arcangeli
873d505e5b fix: don't set buflisted on oil buffers (#220) 2023-11-14 22:41:39 -08:00
Steven Arcangeli
af13ce333f fix: quit after mutations when :wq or similar (#221) 2023-11-13 10:46:08 -08:00
Steven Arcangeli
3727410e48
fix: previewing and editing files on windows (#214) 2023-11-09 21:31:23 -08:00
Steven Arcangeli
6175bd6462
feat: trash support for linux and mac (#165)
* wip: skeleton code for trash adapter

* refactor: split trash implementation for mac and linux

* fix: ensure we create the .Trash/$uid dir

* feat: code complete linux trash implementation

* doc: write up trash features

* feat: code complete mac trash implementation

* cleanup: remove previous, terrible, undocumented trash feature

* fix: always disabled trash

* feat: show original path of trashed files

* doc: add a note about calling actions directly

* fix: bugs in trash implementation

* fix: schedule_wrap in mac trash

* doc: fix typo and line wrapping

* fix: parsing of arguments to :Oil command

* doc: small documentation tweaks

* doc: fix awkward wording in the toggle_trash action

* fix: warning on Windows when delete_to_trash = true

* feat: :Oil --trash can open specific trash directories

* fix: show all trash files in device root

* fix: trash mtime should be sortable

* fix: shorten_path handles optional trailing slash

* refactor: overhaul the UI

* fix: keep trash original path vtext from stacking

* refactor: replace disable_changes with an error filter

* fix: shorten path names in home directory relative to root

* doc: small README format changes

* cleanup: remove unnecessary preserve_undo logic

* test: add a functional test for the freedesktop trash adapter

* test: more functional tests for trash

* fix: schedule a callback to avoid main loop error

* refactor: clean up mutator logic

* doc: some comments and type annotations
2023-11-05 12:40:58 -08:00
Steven Arcangeli
d8f0d91b10 feat: display ../ entry in oil buffers (#166) 2023-11-05 08:00:38 -08:00
Steven Arcangeli
0715f1b0aa fix: line parsing for empty columns 2023-11-05 07:57:54 -08:00
Steven Arcangeli
57db10d748 lint: apply stylua 2023-11-05 07:29:15 -08:00
Steven Arcangeli
126a8a2346 fix: can view drives on Windows 2023-11-05 07:27:28 -08:00
75 changed files with 8222 additions and 1526 deletions

2
.envrc
View file

@ -1 +1,3 @@
export VIRTUAL_ENV=venv
layout python layout python
python -c 'import pyparsing' 2>/dev/null || pip install -r scripts/requirements.txt

View file

@ -33,6 +33,16 @@ body:
description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim. description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim.
validations: validations:
required: true required: true
- type: dropdown
attributes:
label: What is the severity of this bug?
options:
- minor (annoyance)
- tolerable (can work around it)
- breaking (some functionality is broken)
- blocking (cannot use plugin)
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Steps To Reproduce label: Steps To Reproduce

View file

@ -26,6 +26,15 @@ body:
placeholder: I am trying to do X. My current workflow is Y. placeholder: I am trying to do X. My current workflow is Y.
validations: validations:
required: false required: false
- type: dropdown
attributes:
label: What is the significance of this feature?
options:
- nice to have
- strongly desired
- cannot use this plugin without it
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Additional details label: Additional details

View file

@ -0,0 +1,16 @@
name: Remove Question Label on Issue Comment
on: [issue_comment]
jobs:
# Remove the "question" label when a new comment is added.
# This lets me ask a question, tag the issue with "question", and filter out all "question"-tagged
# issues in my "needs triage" filter.
remove_question:
runs-on: ubuntu-latest
if: github.event.sender.login != 'stevearc'
steps:
- uses: actions/checkout@v4
- uses: actions-ecosystem/action-remove-labels@v1
with:
labels: question

View file

@ -0,0 +1,27 @@
name: Request Review
permissions:
pull-requests: write
on:
pull_request_target:
types: [opened, reopened, ready_for_review, synchronize]
branches-ignore:
- "release-please--**"
jobs:
# Request review automatically when PRs are opened
request_review:
runs-on: ubuntu-latest
steps:
- name: Request Review
uses: actions/github-script@v7
if: github.actor != 'stevearc'
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pr = context.payload.pull_request;
github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
reviewers: ['stevearc']
});

View file

@ -1,12 +1,16 @@
#!/bin/bash #!/bin/bash
set -e set -e
PLUGINS="$HOME/.local/share/nvim/site/pack/plugins/start" version="${NVIM_TAG-stable}"
mkdir -p "$PLUGINS" dl_name="nvim-linux-x86_64.appimage"
# The appimage name changed in v0.10.4
wget "https://github.com/neovim/neovim/releases/download/${NVIM_TAG-stable}/nvim.appimage" 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
chmod +x nvim.appimage chmod +x nvim.appimage
./nvim.appimage --appimage-extract >/dev/null ./nvim.appimage --appimage-extract >/dev/null
rm -f nvim.appimage rm -f nvim.appimage
mkdir -p ~/.local/share/nvim mkdir -p ~/.local/share/nvim
mv squashfs-root ~/.local/share/nvim/appimage mv squashfs-root ~/.local/share/nvim/appimage
sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim
/usr/bin/nvim --version

View file

@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- master - master
- stevearc-*
pull_request: pull_request:
branches: branches:
- master - master
@ -13,7 +14,7 @@ jobs:
name: Luacheck name: Luacheck
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Prepare - name: Prepare
run: | run: |
@ -29,20 +30,20 @@ jobs:
name: StyLua name: StyLua
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Stylua - name: Stylua
uses: JohnnyMorganz/stylua-action@v3 uses: JohnnyMorganz/stylua-action@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
version: v0.18.2 version: v2.0.2
args: --check lua tests args: --check lua tests
typecheck: typecheck:
name: typecheck name: typecheck
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: stevearc/nvim-typecheck-action@v1 - uses: stevearc/nvim-typecheck-action@v2
with: with:
path: lua path: lua
@ -51,14 +52,16 @@ jobs:
matrix: matrix:
include: include:
- nvim_tag: v0.8.3 - nvim_tag: v0.8.3
- nvim_tag: v0.9.1 - nvim_tag: v0.9.4
- nvim_tag: v0.10.4
- nvim_tag: v0.11.0
name: Run tests name: Run tests
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
env: env:
NVIM_TAG: ${{ matrix.nvim_tag }} NVIM_TAG: ${{ matrix.nvim_tag }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install Neovim and dependencies - name: Install Neovim and dependencies
run: | run: |
@ -72,7 +75,7 @@ jobs:
name: Update docs name: Update docs
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install Neovim and dependencies - name: Install Neovim and dependencies
run: | run: |
@ -109,12 +112,11 @@ jobs:
- update_docs - update_docs
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: google-github-actions/release-please-action@v3 - uses: googleapis/release-please-action@v4
id: release id: release
with: with:
release-type: simple release-type: simple
package-name: oil.nvim - uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: rickstaa/action-create-tag@v1 - uses: rickstaa/action-create-tag@v1
if: ${{ steps.release.outputs.release_created }} if: ${{ steps.release.outputs.release_created }}
with: with:

7
.gitignore vendored
View file

@ -6,6 +6,9 @@ luac.out
*.zip *.zip
*.tar.gz *.tar.gz
# python bytecode
__pycache__
# Object files # Object files
*.o *.o
*.os *.os
@ -41,6 +44,10 @@ luac.out
.direnv/ .direnv/
.testenv/ .testenv/
venv/
doc/tags doc/tags
scripts/nvim_doc_tools scripts/nvim_doc_tools
scripts/nvim-typecheck-action scripts/nvim-typecheck-action
scripts/benchmark.nvim
perf/tmp/
profile.json

9
.luarc.json Normal file
View file

@ -0,0 +1,9 @@
{
"runtime": {
"version": "LuaJIT",
"pathStrict": true
},
"type": {
"checkTableShape": true
}
}

View file

@ -1,5 +1,299 @@
# Changelog # 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)
### Bug Fixes
* ensure win_options are being set on correct window ([#469](https://github.com/stevearc/oil.nvim/issues/469)) ([30e0438](https://github.com/stevearc/oil.nvim/commit/30e0438ff08f197d7ce4a417445ab97ee72efe2d))
* wrap git rm callback in schedule_wrap ([#475](https://github.com/stevearc/oil.nvim/issues/475)) ([b053744](https://github.com/stevearc/oil.nvim/commit/b05374428e5136d9b6c8e1e8e62a75f82283b1f8))
### Performance Improvements
* **view:** avoid running `is_hidden_file` when `show_hidden` is set ([#471](https://github.com/stevearc/oil.nvim/issues/471)) ([0fcd126](https://github.com/stevearc/oil.nvim/commit/0fcd1263a2e8b6200e2b9fd4ab83d40ed8899c54))
## [2.12.1](https://github.com/stevearc/oil.nvim/compare/v2.12.0...v2.12.1) (2024-08-26)
### Bug Fixes
* gracefully handle trashing file that does not exist ([70337eb](https://github.com/stevearc/oil.nvim/commit/70337eb77f53cbff0b7f54f403d5b2b0a9430935))
* process deletes in dir before moving dir ([349bca8](https://github.com/stevearc/oil.nvim/commit/349bca8c3eae4ab78629ed63ee55cc3458a367c0))
## [2.12.0](https://github.com/stevearc/oil.nvim/compare/v2.11.0...v2.12.0) (2024-08-17)
### Features
* add support for `mini.icons` ([#439](https://github.com/stevearc/oil.nvim/issues/439)) ([a543ea5](https://github.com/stevearc/oil.nvim/commit/a543ea598eaef3363fe253e0e11837c1404eb04d))
* allow bufnr optional parameter for get_current_dir function ([#440](https://github.com/stevearc/oil.nvim/issues/440)) ([cc23325](https://github.com/stevearc/oil.nvim/commit/cc2332599f8944076fba29ff7960729b3fcdd71b))
* disable cursor in preview window ([#433](https://github.com/stevearc/oil.nvim/issues/433)) ([b15e4c1](https://github.com/stevearc/oil.nvim/commit/b15e4c1e647b9ddbb75a31caeb720b3b3ce4db54))
### Bug Fixes
* add compatibility for Lua 5.1 ([#456](https://github.com/stevearc/oil.nvim/issues/456)) ([b39a789](https://github.com/stevearc/oil.nvim/commit/b39a78959f3f69e9c1bf43c2634bbddf0af51c3e))
* correctly check if `mini.icons` is actually setup ([#441](https://github.com/stevearc/oil.nvim/issues/441)) ([d5e5657](https://github.com/stevearc/oil.nvim/commit/d5e56574f896120b78cdf56dc1132e76057f8877))
* cursor sometimes disappears after making changes ([#438](https://github.com/stevearc/oil.nvim/issues/438)) ([b5a1abf](https://github.com/stevearc/oil.nvim/commit/b5a1abfde00eead6814cae3321e4c90ff98cfff1))
* Force standard C locale when getting `ls` input for parsing in SSH ([#455](https://github.com/stevearc/oil.nvim/issues/455)) ([71c972f](https://github.com/stevearc/oil.nvim/commit/71c972fbd218723a3c15afcb70421f67340f5a6d))
* handle rare case where file watcher outlives buffer ([fcca212](https://github.com/stevearc/oil.nvim/commit/fcca212c2e966fc3dec1d4baf888e670631d25d1))
* Handle users and groups with spaces over SSH ([#448](https://github.com/stevearc/oil.nvim/issues/448)) ([a6cea1a](https://github.com/stevearc/oil.nvim/commit/a6cea1a5b9bc9351769fe09a547c62fe4b669abd))
* set floating window win_options when buffer changes ([#432](https://github.com/stevearc/oil.nvim/issues/432)) ([b0a6cf9](https://github.com/stevearc/oil.nvim/commit/b0a6cf98982cdcf82b19b0029b734bbbcd24bcc4))
## [2.11.0](https://github.com/stevearc/oil.nvim/compare/v2.10.0...v2.11.0) (2024-07-01)
### Features
* case insensitive sorting ([#429](https://github.com/stevearc/oil.nvim/issues/429)) ([2077cc3](https://github.com/stevearc/oil.nvim/commit/2077cc3358f327aca16c376cdde6ea0b07f14449))
* rename experimental_watch_for_changes -&gt; watch_for_changes ([c7c7ce5](https://github.com/stevearc/oil.nvim/commit/c7c7ce5bd47030ee9c60a859f25695647610b8bd))
* support preview from floating window ([#403](https://github.com/stevearc/oil.nvim/issues/403)) ([59b3dab](https://github.com/stevearc/oil.nvim/commit/59b3dab6f79e147a0d694ee72c26ae883d323340))
### Bug Fixes
* bug in buffer rendering race condition handling ([f6df58a](https://github.com/stevearc/oil.nvim/commit/f6df58ad370f45dbc18c42ffbaefbcf27df14036))
* correctly check group permissions in unix ([#428](https://github.com/stevearc/oil.nvim/issues/428)) ([65c53db](https://github.com/stevearc/oil.nvim/commit/65c53dbe4f2140236590a7568a5f22a77d16be39))
* increase loading display delay to avoid flicker ([#424](https://github.com/stevearc/oil.nvim/issues/424)) ([4c574cf](https://github.com/stevearc/oil.nvim/commit/4c574cf4a2de736d2662d52ce086d8bdf87c49df))
## [2.10.0](https://github.com/stevearc/oil.nvim/compare/v2.9.0...v2.10.0) (2024-06-16)
### Features
* add copy filename action ([#391](https://github.com/stevearc/oil.nvim/issues/391)) ([bbc0e67](https://github.com/stevearc/oil.nvim/commit/bbc0e67eebc15342e73b146a50d9b52e6148161b))
* keymap actions can be parameterized ([96368e1](https://github.com/stevearc/oil.nvim/commit/96368e13e9b1aaacc570e4825b8787307f0d05e1))
### Bug Fixes
* change unknown action name from error to notification ([e5eb20e](https://github.com/stevearc/oil.nvim/commit/e5eb20e88fc03bf89f371032de77f176158b41d3))
* error opening command window from oil float ([#378](https://github.com/stevearc/oil.nvim/issues/378)) ([06a19f7](https://github.com/stevearc/oil.nvim/commit/06a19f77f1a1da37b675635e6f9c5b5d50bcaacd))
* hack around glob issues in LSP rename operations ([#386](https://github.com/stevearc/oil.nvim/issues/386)) ([e5312c3](https://github.com/stevearc/oil.nvim/commit/e5312c3a801e7274fa14e6a56aa10a618fed80c3))
* incorrect default config actions ([#414](https://github.com/stevearc/oil.nvim/issues/414)) ([c82b26e](https://github.com/stevearc/oil.nvim/commit/c82b26eb4ba35c0eb7ec38d88dd400597fb34883))
* notify when changing the current directory ([#406](https://github.com/stevearc/oil.nvim/issues/406)) ([18272ab](https://github.com/stevearc/oil.nvim/commit/18272aba9d00a3176a5443d50dbb4464acc167bd))
* throw error on vim.has call within the lsp/workspace.lua ([#411](https://github.com/stevearc/oil.nvim/issues/411)) ([61f1967](https://github.com/stevearc/oil.nvim/commit/61f1967222365474c6cf7953c569cc94dbcc7acd))
* vim.notify call error ([76bfc25](https://github.com/stevearc/oil.nvim/commit/76bfc25520e4edc98d089d023b4ed06013639849))
## [2.9.0](https://github.com/stevearc/oil.nvim/compare/v2.8.0...v2.9.0) (2024-05-16)
### Features
* can restore Oil progress window when minimized ([fa3820e](https://github.com/stevearc/oil.nvim/commit/fa3820ebf1e8ccf5c7c0f3626d499b2c1aa8bc50))
* experimental support for git operations ([#290](https://github.com/stevearc/oil.nvim/issues/290)) ([1f05774](https://github.com/stevearc/oil.nvim/commit/1f05774e1c2dbc1940104b5c950d5c7b65ec6e0b))
### Bug Fixes
* duplicate create actions ([#334](https://github.com/stevearc/oil.nvim/issues/334)) ([354c530](https://github.com/stevearc/oil.nvim/commit/354c53080a6d7f4f0b2f0cc12e53bede2480b9e5))
* error when opening files from floating oil window ([#355](https://github.com/stevearc/oil.nvim/issues/355)) ([2bc56ad](https://github.com/stevearc/oil.nvim/commit/2bc56ad68afd092af1b2e77dd5d61e156938564c))
* git mv errors when moving empty directory ([#358](https://github.com/stevearc/oil.nvim/issues/358)) ([6a7a10b](https://github.com/stevearc/oil.nvim/commit/6a7a10b6117aface6a25b54906140ad4f7fdabfc))
* gracefully handle new dirs with trailing backslash on windows ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([be0a1ec](https://github.com/stevearc/oil.nvim/commit/be0a1ecbf0541692a1b9b6e8ea15f5f57db8747a))
* icon column highlight parameter ([#366](https://github.com/stevearc/oil.nvim/issues/366)) ([752563c](https://github.com/stevearc/oil.nvim/commit/752563c59d64a5764cc0743d4fa0aac9ae4a2640))
* race condition when entering oil buffer ([#321](https://github.com/stevearc/oil.nvim/issues/321)) ([c86e484](https://github.com/stevearc/oil.nvim/commit/c86e48407b8a45f9aa8acb2b4512b384ea1eec84))
* **ssh:** bad argument when editing files over ssh ([#370](https://github.com/stevearc/oil.nvim/issues/370)) ([aa0c00c](https://github.com/stevearc/oil.nvim/commit/aa0c00c7fd51982ac476d165cd021f348cf5ea71))
* **ssh:** config option to pass extra args to SCP ([#340](https://github.com/stevearc/oil.nvim/issues/340)) ([3abb607](https://github.com/stevearc/oil.nvim/commit/3abb6077d7d6b09f5eb794b8764223b3027f6807))
* **ssh:** garbled output when directory has broken symlinks ([bcfc0a2](https://github.com/stevearc/oil.nvim/commit/bcfc0a2e01def5019aa14fac2fc6de20dedb6d3d))
* support visual mode when preview window is open ([#315](https://github.com/stevearc/oil.nvim/issues/315)) ([f41d7e7](https://github.com/stevearc/oil.nvim/commit/f41d7e7cd8e4028b03c35d847b4396790ac8bb2d))
* **windows:** convert posix paths before matching LSP watch globs ([#374](https://github.com/stevearc/oil.nvim/issues/374)) ([f630887](https://github.com/stevearc/oil.nvim/commit/f630887cd845a7341bc16488fe8aaecffe3aaa8a))
* **windows:** file operation preview uses only backslash path separator ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([96f0983](https://github.com/stevearc/oil.nvim/commit/96f0983e754694e592d4313f583cd31eaebfa80d))
* **windows:** navigating into drive letter root directories ([#341](https://github.com/stevearc/oil.nvim/issues/341)) ([f3a31eb](https://github.com/stevearc/oil.nvim/commit/f3a31eba24587bc038592103d8f7e64648292115))
* **windows:** treat both backslash and frontslash as path separators ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([3b3a6b2](https://github.com/stevearc/oil.nvim/commit/3b3a6b23a120e69ddc980c9d32840ecd521fbff9))
## [2.8.0](https://github.com/stevearc/oil.nvim/compare/v2.7.0...v2.8.0) (2024-04-19)
### Features
* add user autocmds before and after performing actions ([#310](https://github.com/stevearc/oil.nvim/issues/310)) ([e462a34](https://github.com/stevearc/oil.nvim/commit/e462a3446505185adf063566f5007771b69027a1))
### Bug Fixes
* output suppressed when opening files ([#348](https://github.com/stevearc/oil.nvim/issues/348)) ([6c48ac7](https://github.com/stevearc/oil.nvim/commit/6c48ac7dc679c5694a2c0375a5e67773e31d8157))
* **ssh:** escape all file paths for the ssh adapter ([#353](https://github.com/stevearc/oil.nvim/issues/353)) ([8bb35eb](https://github.com/stevearc/oil.nvim/commit/8bb35eb81a48f14c4a1ef480c2bbb87ceb7cd8bb))
## [2.7.0](https://github.com/stevearc/oil.nvim/compare/v2.6.1...v2.7.0) (2024-03-13)
### Features
* add ability to alter lsp file operation timeout ([#317](https://github.com/stevearc/oil.nvim/issues/317)) ([29a06fc](https://github.com/stevearc/oil.nvim/commit/29a06fcc906f57894c1bc768219ba590e03d1121))
* add border config for SSH and keymaps help window ([#299](https://github.com/stevearc/oil.nvim/issues/299)) ([e27cc4e](https://github.com/stevearc/oil.nvim/commit/e27cc4e13812f96c0851de67015030a823cc0fbd))
* do not close preview when switching dirs ([#277](https://github.com/stevearc/oil.nvim/issues/277)) ([bf753c3](https://github.com/stevearc/oil.nvim/commit/bf753c3e3f8736939ad5597f92329dfe7b1df4f5))
* experimental option to watch directory for changes ([#292](https://github.com/stevearc/oil.nvim/issues/292)) ([bcfe7d1](https://github.com/stevearc/oil.nvim/commit/bcfe7d1ec5bbf41dd78726f579a363028d208c1a))
* use natural sort order by default ([#328](https://github.com/stevearc/oil.nvim/issues/328)) ([71b076b](https://github.com/stevearc/oil.nvim/commit/71b076b3afb40663222564c74162db555aeee62d))
### Bug Fixes
* actions.open_external uses explorer.exe in WSL ([#273](https://github.com/stevearc/oil.nvim/issues/273)) ([6953c2c](https://github.com/stevearc/oil.nvim/commit/6953c2c17d8ae7454b28c44c8767eebede312e6f))
* close preview window when leaving oil buffer ([#296](https://github.com/stevearc/oil.nvim/issues/296)) ([132b4ea](https://github.com/stevearc/oil.nvim/commit/132b4ea0740c417b9d717411cab4cf187e1fd095))
* correctly reset bufhidden for formerly previewed buffers ([#291](https://github.com/stevearc/oil.nvim/issues/291)) ([0de8e60](https://github.com/stevearc/oil.nvim/commit/0de8e60e3d8d3d1ff9378526b4722f1ea326e1cb))
* potential leak in experimental file watcher ([c437f3c](https://github.com/stevearc/oil.nvim/commit/c437f3c5b0da0a9cc6a222d87212cce11b80ba75))
* spurious exits from faulty :wq detection ([#221](https://github.com/stevearc/oil.nvim/issues/221)) ([e045ee3](https://github.com/stevearc/oil.nvim/commit/e045ee3b4e06cafd7a6a2acac10f2558e611eaf8))
* window options sometimes not set in oil buffer ([#287](https://github.com/stevearc/oil.nvim/issues/287)) ([17d71eb](https://github.com/stevearc/oil.nvim/commit/17d71eb3d88a79dbc87c6245f8490853a5c38092))
* **windows:** can delete non-ascii filenames to trash ([#323](https://github.com/stevearc/oil.nvim/issues/323)) ([18dfd24](https://github.com/stevearc/oil.nvim/commit/18dfd2458dc741fea683357a17aaa95870b25a3c))
## [2.6.1](https://github.com/stevearc/oil.nvim/compare/v2.6.0...v2.6.1) (2024-01-16)
### Bug Fixes
* crash when LSP client workspace_folders is nil ([#269](https://github.com/stevearc/oil.nvim/issues/269)) ([c4cc824](https://github.com/stevearc/oil.nvim/commit/c4cc8240f1c71defcb67c45da96e44b968d29e5f))
* diagnostic float would not open if scope=cursor ([#275](https://github.com/stevearc/oil.nvim/issues/275)) ([a1af7a1](https://github.com/stevearc/oil.nvim/commit/a1af7a1b593d8d28581ef0de82a6977721601afa))
* **lsp_rename:** handle absolute path glob filters ([#279](https://github.com/stevearc/oil.nvim/issues/279)) ([ec24334](https://github.com/stevearc/oil.nvim/commit/ec24334471e7ccbfb7488910159245dc7327a07d))
* **trash:** mac error deleting dangling symbolic links to trash ([#251](https://github.com/stevearc/oil.nvim/issues/251)) ([49b2b3f](https://github.com/stevearc/oil.nvim/commit/49b2b3f4a50bcd546decf751e5834de9b6f38d97))
### Performance Improvements
* **windows:** use a single powershell process for trash operations ([#271](https://github.com/stevearc/oil.nvim/issues/271)) ([e71b6ca](https://github.com/stevearc/oil.nvim/commit/e71b6caa95bd29225536df64fdcd8fb0f758bb09))
## [2.6.0](https://github.com/stevearc/oil.nvim/compare/v2.5.0...v2.6.0) (2024-01-03)
### Features
* **trash:** support for deleting to windows recycle bin ([#243](https://github.com/stevearc/oil.nvim/issues/243)) ([553b7a0](https://github.com/stevearc/oil.nvim/commit/553b7a0ac129c0e7a7bbde72f9fbfe7c9f4be6c3))
## [2.5.0](https://github.com/stevearc/oil.nvim/compare/v2.4.1...v2.5.0) (2023-12-26)
### Features
* actions for sending oil entries to quickfix ([#249](https://github.com/stevearc/oil.nvim/issues/249)) ([3ffb830](https://github.com/stevearc/oil.nvim/commit/3ffb8309e6eda961c7edb9ecbe6a340fe9e24b43))
* add 'update_on_cursor_moved' option to preview window ([#250](https://github.com/stevearc/oil.nvim/issues/250)) ([ea612fe](https://github.com/stevearc/oil.nvim/commit/ea612fe926a24ea20b2b3856e1ba60bdaaae9383))
* allow multiple hlgroups inside one column ([#240](https://github.com/stevearc/oil.nvim/issues/240)) ([a173b57](https://github.com/stevearc/oil.nvim/commit/a173b5776c66a31ce08552677c1eae7ab015835f))
* constrain_cursor option (closes [#257](https://github.com/stevearc/oil.nvim/issues/257)) ([71b1ef5](https://github.com/stevearc/oil.nvim/commit/71b1ef5edfcee7c58fe611fdd79bfafcb9fb0531))
* option to auto-save files affected by will_rename_files ([#218](https://github.com/stevearc/oil.nvim/issues/218)) ([48d8ea8](https://github.com/stevearc/oil.nvim/commit/48d8ea8f4a6590ef7339ff0fdb97cef3e238dd86))
* refresh action also clears search highlight ([#228](https://github.com/stevearc/oil.nvim/issues/228)) ([8283457](https://github.com/stevearc/oil.nvim/commit/82834573bbca27c240f30087ff642b807ed1872a))
* support all LSP workspace file operations ([#264](https://github.com/stevearc/oil.nvim/issues/264)) ([250e0af](https://github.com/stevearc/oil.nvim/commit/250e0af7a54d750792be8b1d6165b76b6603a867))
### Bug Fixes
* constrain cursor when entering insert mode ([a60c6d1](https://github.com/stevearc/oil.nvim/commit/a60c6d10fd66de275c1d00451c918104ef9b6d10))
* handle opening oil from buffers with foreign schemes ([#256](https://github.com/stevearc/oil.nvim/issues/256)) ([22ab2ce](https://github.com/stevearc/oil.nvim/commit/22ab2ce1d56832588a634e7737404d9344698bd3))
* **trash:** error deleting dangling symbolic links to trash ([#251](https://github.com/stevearc/oil.nvim/issues/251)) ([5d9e436](https://github.com/stevearc/oil.nvim/commit/5d9e4368d49aec00b1e0d9ea520e1403ad6ad634))
* willRename source path ([#248](https://github.com/stevearc/oil.nvim/issues/248)) ([24027ed](https://github.com/stevearc/oil.nvim/commit/24027ed8d7f3ee5c38cfd713915e2e16d89e79b3))
### Performance Improvements
* speed up session loading ([#246](https://github.com/stevearc/oil.nvim/issues/246)) ([b3c24f4](https://github.com/stevearc/oil.nvim/commit/b3c24f4b3b2d38483241292a330cd6eb00734dac))
## [2.4.1](https://github.com/stevearc/oil.nvim/compare/v2.4.0...v2.4.1) (2023-12-01)
### Bug Fixes
* buffer data cleared when setting buflisted = false ([303f318](https://github.com/stevearc/oil.nvim/commit/303f31895e7ce10df250c88c7a5f7d8d9c56f0fc))
* bug copying file multiple times ([05cb825](https://github.com/stevearc/oil.nvim/commit/05cb8257cb9257144e63f41ccfe5a41ba3d1003c))
* crash in ssh and trash adapter detail columns ([#235](https://github.com/stevearc/oil.nvim/issues/235)) ([e89a8f8](https://github.com/stevearc/oil.nvim/commit/e89a8f8adeef2dfab851fd056d38ee7afc97c249))
* oil.select respects splitbelow and splitright ([#233](https://github.com/stevearc/oil.nvim/issues/233)) ([636989b](https://github.com/stevearc/oil.nvim/commit/636989b603fb95032efa9d3e1b3323c8bb533e91))
* preserve buflisted when re-opening oil buffers ([#220](https://github.com/stevearc/oil.nvim/issues/220)) ([6566f45](https://github.com/stevearc/oil.nvim/commit/6566f457e44498adc6835bed5402b38386fa1438))
## [2.4.0](https://github.com/stevearc/oil.nvim/compare/v2.3.0...v2.4.0) (2023-11-15)
### Features
* display ../ entry in oil buffers ([#166](https://github.com/stevearc/oil.nvim/issues/166)) ([d8f0d91](https://github.com/stevearc/oil.nvim/commit/d8f0d91b10ec53da722b0909697b57c2f5368245))
* trash support for linux and mac ([#165](https://github.com/stevearc/oil.nvim/issues/165)) ([6175bd6](https://github.com/stevearc/oil.nvim/commit/6175bd646272335c8db93264760760d8f2a611d5))
### Bug Fixes
* can view drives on Windows ([126a8a2](https://github.com/stevearc/oil.nvim/commit/126a8a23465312683edf646555b3031bfe56796d))
* don't set buflisted on oil buffers ([#220](https://github.com/stevearc/oil.nvim/issues/220)) ([873d505](https://github.com/stevearc/oil.nvim/commit/873d505e5bfdd65317ea97ead8faa6c56bac04c0))
* line parsing for empty columns ([0715f1b](https://github.com/stevearc/oil.nvim/commit/0715f1b0aacef70573ed6300c12039831fbd81c3))
* previewing and editing files on windows ([#214](https://github.com/stevearc/oil.nvim/issues/214)) ([3727410](https://github.com/stevearc/oil.nvim/commit/3727410e4875ad8ba339c585859a9391d643b9ed))
* quit after mutations when :wq or similar ([#221](https://github.com/stevearc/oil.nvim/issues/221)) ([af13ce3](https://github.com/stevearc/oil.nvim/commit/af13ce333f89c54a47e6772b55fed2438ee6957c))
## [2.3.0](https://github.com/stevearc/oil.nvim/compare/v2.2.0...v2.3.0) (2023-11-04) ## [2.3.0](https://github.com/stevearc/oil.nvim/compare/v2.2.0...v2.3.0) (2023-11-04)

View file

@ -1,27 +1,66 @@
.PHONY: all doc test lint fastlint clean ## help: print this help message
.PHONY: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
## all: generate docs, lint, and run tests
.PHONY: all
all: doc lint test all: doc lint test
doc: scripts/nvim_doc_tools venv:
python scripts/main.py generate python3 -m venv venv
python scripts/main.py lint venv/bin/pip install -r scripts/requirements.txt
## 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: test:
./run_tests.sh ./run_tests.sh
## lint: run linters and LuaLS typechecking
.PHONY: lint
lint: scripts/nvim-typecheck-action fastlint lint: scripts/nvim-typecheck-action fastlint
./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua ./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua
fastlint: scripts/nvim_doc_tools ## fastlint: run only fast linters
python scripts/main.py lint .PHONY: fastlint
fastlint: scripts/nvim_doc_tools venv
venv/bin/python scripts/main.py lint
luacheck lua tests --formatter plain luacheck lua tests --formatter plain
stylua --check lua tests 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: scripts/nvim_doc_tools:
git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools
scripts/nvim-typecheck-action: scripts/nvim-typecheck-action:
git clone https://github.com/stevearc/nvim-typecheck-action 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: clean:
rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv perf/tmp profile.json

183
README.md
View file

@ -11,6 +11,8 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94
- [Quick start](#quick-start) - [Quick start](#quick-start)
- [Options](#options) - [Options](#options)
- [Adapters](#adapters) - [Adapters](#adapters)
- [Recipes](#recipes)
- [Third-party extensions](#third-party-extensions)
- [API](#api) - [API](#api)
- [FAQ](#faq) - [FAQ](#faq)
@ -19,7 +21,9 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94
## Requirements ## Requirements
- Neovim 0.8+ - Neovim 0.8+
- (optional) [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons - Icon provider plugin (optional)
- [mini.icons](https://github.com/nvim-mini/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 ## Installation
@ -31,9 +35,14 @@ oil.nvim supports all the usual plugin managers
```lua ```lua
{ {
'stevearc/oil.nvim', 'stevearc/oil.nvim',
---@module 'oil'
---@type oil.SetupOpts
opts = {}, opts = {},
-- Optional dependencies -- Optional dependencies
dependencies = { "nvim-tree/nvim-web-devicons" }, 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,
} }
``` ```
@ -43,11 +52,13 @@ oil.nvim supports all the usual plugin managers
<summary>Packer</summary> <summary>Packer</summary>
```lua ```lua
require('packer').startup(function() require("packer").startup(function()
use { use({
'stevearc/oil.nvim', "stevearc/oil.nvim",
config = function() require('oil').setup() end config = function()
} require("oil").setup()
end,
})
end) end)
``` ```
@ -57,9 +68,9 @@ end)
<summary>Paq</summary> <summary>Paq</summary>
```lua ```lua
require "paq" { require("paq")({
{'stevearc/oil.nvim'}; { "stevearc/oil.nvim" },
} })
``` ```
</details> </details>
@ -124,7 +135,7 @@ You can open a directory with `:edit <path>` or `:Oil <path>`. To open oil in a
```lua ```lua
require("oil").setup({ require("oil").setup({
-- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`)
-- Set to false if you still want to use netrw. -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories.
default_file_explorer = true, default_file_explorer = true,
-- Id is automatically added at the beginning, and name at the end -- Id is automatically added at the beginning, and name at the end
-- See :help oil-columns -- See :help oil-columns
@ -152,16 +163,29 @@ require("oil").setup({
}, },
-- Send deleted files to the trash instead of permanently deleting them (:help oil-trash) -- Send deleted files to the trash instead of permanently deleting them (:help oil-trash)
delete_to_trash = false, delete_to_trash = false,
-- Skip the confirmation popup for simple operations -- Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits)
skip_confirm_for_simple_edits = false, skip_confirm_for_simple_edits = false,
-- Change this to customize the command used when deleting to trash
trash_command = "trash-put",
-- Selecting a new/moved/renamed file or directory will prompt you to save changes first -- Selecting a new/moved/renamed file or directory will prompt you to save changes first
-- (:help prompt_save_on_select_new_entry)
prompt_save_on_select_new_entry = true, prompt_save_on_select_new_entry = true,
-- Oil will automatically delete hidden buffers after this delay -- Oil will automatically delete hidden buffers after this delay
-- You can set the delay to false to disable cleanup entirely -- You can set the delay to false to disable cleanup entirely
-- Note that the cleanup process only starts when none of the oil buffers are currently displayed -- Note that the cleanup process only starts when none of the oil buffers are currently displayed
cleanup_delay_ms = 2000, 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
-- Set to "unmodified" to only save unmodified buffers
autosave_changes = false,
},
-- Constrain the cursor to the editable parts of the oil buffer
-- Set to `false` to disable, or "name" to keep it on the file names
constrain_cursor = "editable",
-- Set to true to watch the filesystem for changes and reload oil
watch_for_changes = false,
-- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap
-- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" })
-- Additionally, if it is a string that matches "actions.<name>", -- Additionally, if it is a string that matches "actions.<name>",
@ -169,21 +193,22 @@ require("oil").setup({
-- Set to `false` to remove a keymap -- Set to `false` to remove a keymap
-- See :help oil-actions for a list of all available actions -- See :help oil-actions for a list of all available actions
keymaps = { keymaps = {
["g?"] = "actions.show_help", ["g?"] = { "actions.show_help", mode = "n" },
["<CR>"] = "actions.select", ["<CR>"] = "actions.select",
["<C-s>"] = "actions.select_vsplit", ["<C-s>"] = { "actions.select", opts = { vertical = true } },
["<C-h>"] = "actions.select_split", ["<C-h>"] = { "actions.select", opts = { horizontal = true } },
["<C-t>"] = "actions.select_tab", ["<C-t>"] = { "actions.select", opts = { tab = true } },
["<C-p>"] = "actions.preview", ["<C-p>"] = "actions.preview",
["<C-c>"] = "actions.close", ["<C-c>"] = { "actions.close", mode = "n" },
["<C-l>"] = "actions.refresh", ["<C-l>"] = "actions.refresh",
["-"] = "actions.parent", ["-"] = { "actions.parent", mode = "n" },
["_"] = "actions.open_cwd", ["_"] = { "actions.open_cwd", mode = "n" },
["`"] = "actions.cd", ["`"] = { "actions.cd", mode = "n" },
["~"] = "actions.tcd", ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = "actions.change_sort", ["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external", ["gx"] = "actions.open_external",
["g."] = "actions.toggle_hidden", ["g."] = { "actions.toggle_hidden", mode = "n" },
["g\\"] = { "actions.toggle_trash", mode = "n" },
}, },
-- Set to false to disable all of the above keymaps -- Set to false to disable all of the above keymaps
use_default_keymaps = true, use_default_keymaps = true,
@ -192,37 +217,82 @@ require("oil").setup({
show_hidden = false, show_hidden = false,
-- This function defines what is considered a "hidden" file -- This function defines what is considered a "hidden" file
is_hidden_file = function(name, bufnr) is_hidden_file = function(name, bufnr)
return vim.startswith(name, ".") local m = name:match("^%.")
return m ~= nil
end, end,
-- This function defines what will never be shown, even when `show_hidden` is set -- This function defines what will never be shown, even when `show_hidden` is set
is_always_hidden = function(name, bufnr) is_always_hidden = function(name, bufnr)
return false return false
end, 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 and directory names case insensitive
case_insensitive = false,
sort = { sort = {
-- sort order can be "asc" or "desc" -- sort order can be "asc" or "desc"
-- see :help oil-columns to see which columns are sortable -- see :help oil-columns to see which columns are sortable
{ "type", "asc" }, { "type", "asc" },
{ "name", "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
add = function(path)
return false
end,
mv = function(src_path, dest_path)
return false
end,
rm = function(path)
return false
end,
}, },
-- Configuration for the floating window in oil.open_float -- Configuration for the floating window in oil.open_float
float = { float = {
-- Padding around the floating window -- Padding around the floating window
padding = 2, 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_width = 0,
max_height = 0, max_height = 0,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, 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. -- This is the config that will be passed to nvim_open_win.
-- Change values here to customize the layout -- Change values here to customize the layout
override = function(conf) override = function(conf)
return conf return conf
end, end,
}, },
-- Configuration for the actions floating preview window -- Configuration for the file preview window
preview = { 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 = {
-- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- 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. -- 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" -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
@ -239,7 +309,7 @@ require("oil").setup({
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window -- optionally define an integer/float for the exact height of the preview window
height = nil, height = nil,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -252,12 +322,20 @@ require("oil").setup({
max_height = { 10, 0.9 }, max_height = { 10, 0.9 },
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
height = nil, height = nil,
border = "rounded", border = nil,
minimized_border = "none", minimized_border = "none",
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
}, },
-- Configuration for the floating SSH window
ssh = {
border = nil,
},
-- Configuration for the floating keymaps help window
keymaps_help = {
border = nil,
},
}) })
``` ```
@ -277,7 +355,31 @@ nvim oil-ssh://[username@]hostname[:port]/[path]
This may look familiar. In fact, this is the same url format that netrw uses. 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 (`rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`). 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 ## API
@ -290,13 +392,14 @@ Note that at the moment the ssh adapter does not support Windows machines, and i
- [set_sort(sort)](doc/api.md#set_sortsort) - [set_sort(sort)](doc/api.md#set_sortsort)
- [set_is_hidden_file(is_hidden_file)](doc/api.md#set_is_hidden_fileis_hidden_file) - [set_is_hidden_file(is_hidden_file)](doc/api.md#set_is_hidden_fileis_hidden_file)
- [toggle_hidden()](doc/api.md#toggle_hidden) - [toggle_hidden()](doc/api.md#toggle_hidden)
- [get_current_dir()](doc/api.md#get_current_dir) - [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr)
- [open_float(dir)](doc/api.md#open_floatdir) - [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb)
- [toggle_float(dir)](doc/api.md#toggle_floatdir) - [toggle_float(dir, opts, cb)](doc/api.md#toggle_floatdir-opts-cb)
- [open(dir)](doc/api.md#opendir) - [open(dir, opts, cb)](doc/api.md#opendir-opts-cb)
- [close()](doc/api.md#close) - [close(opts)](doc/api.md#closeopts)
- [open_preview(opts, callback)](doc/api.md#open_previewopts-callback)
- [select(opts, callback)](doc/api.md#selectopts-callback) - [select(opts, callback)](doc/api.md#selectopts-callback)
- [save(opts)](doc/api.md#saveopts) - [save(opts, cb)](doc/api.md#saveopts-cb)
- [setup(opts)](doc/api.md#setupopts) - [setup(opts)](doc/api.md#setupopts)
<!-- /API --> <!-- /API -->
@ -318,7 +421,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) - 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 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/echasnovski/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/nvim-mini/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 If you don't need those features specifically, check out the alternatives listed below
@ -334,7 +437,7 @@ If you don't need those features specifically, check out the alternatives listed
**A:** **A:**
- [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. - [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.
- [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. - [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. - [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). - [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).

View file

@ -9,13 +9,14 @@
- [set_sort(sort)](#set_sortsort) - [set_sort(sort)](#set_sortsort)
- [set_is_hidden_file(is_hidden_file)](#set_is_hidden_fileis_hidden_file) - [set_is_hidden_file(is_hidden_file)](#set_is_hidden_fileis_hidden_file)
- [toggle_hidden()](#toggle_hidden) - [toggle_hidden()](#toggle_hidden)
- [get_current_dir()](#get_current_dir) - [get_current_dir(bufnr)](#get_current_dirbufnr)
- [open_float(dir)](#open_floatdir) - [open_float(dir, opts, cb)](#open_floatdir-opts-cb)
- [toggle_float(dir)](#toggle_floatdir) - [toggle_float(dir, opts, cb)](#toggle_floatdir-opts-cb)
- [open(dir)](#opendir) - [open(dir, opts, cb)](#opendir-opts-cb)
- [close()](#close) - [close(opts)](#closeopts)
- [open_preview(opts, callback)](#open_previewopts-callback)
- [select(opts, callback)](#selectopts-callback) - [select(opts, callback)](#selectopts-callback)
- [save(opts)](#saveopts) - [save(opts, cb)](#saveopts-cb)
- [setup(opts)](#setupopts) - [setup(opts)](#setupopts)
<!-- /TOC --> <!-- /TOC -->
@ -58,18 +59,23 @@ Change the display columns for oil
`set_sort(sort)` \ `set_sort(sort)` \
Change the sort order for oil Change the sort order for oil
| Param | Type | Desc | | Param | Type | Desc |
| ----- | ---------- | ---- | | ----- | ---------------- | ------------------------------------------------------------------------------------- |
| sort | `string[]` | [] | | sort | `oil.SortSpec[]` | List of columns plus direction. See :help oil-columns to see which ones are sortable. |
**Examples:**
```lua
require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } })
```
## set_is_hidden_file(is_hidden_file) ## set_is_hidden_file(is_hidden_file)
`set_is_hidden_file(is_hidden_file)` \ `set_is_hidden_file(is_hidden_file)` \
Change how oil determines if the file is hidden Change how oil determines if the file is hidden
| Param | Type | Desc | | Param | Type | Desc |
| -------------- | ----------------------------------------------------- | -------------------------------------------- | | -------------- | ------------------------------------------------ | -------------------------------------------- |
| is_hidden_file | `fun(filename: string, bufnr: nil\|integer): boolean` | Return true if the file/dir should be hidden | | is_hidden_file | `fun(filename: string, bufnr: integer): boolean` | Return true if the file/dir should be hidden |
## toggle_hidden() ## toggle_hidden()
@ -77,79 +83,123 @@ Change how oil determines if the file is hidden
Toggle hidden files and directories Toggle hidden files and directories
## get_current_dir() ## get_current_dir(bufnr)
`get_current_dir(): nil|string` \ `get_current_dir(bufnr): nil|string` \
Get the current directory Get the current directory
| Param | Type | Desc |
| ----- | -------------- | ---- |
| bufnr | `nil\|integer` | |
## open_float(dir) ## open_float(dir, opts, cb)
`open_float(dir)` \ `open_float(dir, opts, cb)` \
Open oil browser in a floating window Open oil browser in a floating window
| Param | Type | Desc | | 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 | | 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 |
## toggle_float(dir) ## toggle_float(dir, opts, cb)
`toggle_float(dir)` \ `toggle_float(dir, opts, cb)` \
Open oil browser in a floating window, or close it if open Open oil browser in a floating window, or close it if open
| Param | Type | Desc | | 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 | | 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 |
## open(dir) ## open(dir, opts, cb)
`open(dir)` \ `open(dir, opts, cb)` \
Open oil browser for a directory Open oil browser for a directory
| Param | Type | Desc | | 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 | | 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 |
## close() ## close(opts)
`close()` \ `close(opts)` \
Restore the buffer that was present when oil was opened 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, callback)` \
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 |
## select(opts, callback) ## select(opts, callback)
`select(opts, callback)` \ `select(opts, callback)` \
Select the entry under the cursor Select the entry under the cursor
| Param | Type | Desc | | | Param | Type | Desc |
| -------- | ---------------------------- | -------------------------------------------------- | ---------------------------------------------------- | | ----------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| opts | `nil\|table` | | | | opts | `nil\|oil.SelectOpts` | |
| | vertical | `boolean` | Open the buffer in a vertical split | | >vertical | `nil\|boolean` | Open the buffer in a vertical split |
| | horizontal | `boolean` | Open the buffer in a horizontal split | | >horizontal | `nil\|boolean` | Open the buffer in a horizontal split |
| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | | >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| | preview | `boolean` | Open the buffer in a preview window | | >tab | `nil\|boolean` | Open the buffer in a new tab |
| | tab | `boolean` | Open the buffer in a new tab | | >close | `nil\|boolean` | Close the original oil buffer once selection is made |
| | close | `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 | | | callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened |
## save(opts) ## save(opts, cb)
`save(opts)` \ `save(opts, cb)` \
Save all changes Save all changes
| Param | Type | Desc | | | Param | Type | Desc |
| ----- | ------------ | -------------- | ------------------------------------------------------------------------------------------- | | -------- | ---------------------------- | ------------------------------------------------------------------------------------------- |
| opts | `nil\|table` | | | | opts | `nil\|table` | |
| | confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | | >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:**
<pre>
If you provide your own callback function, there will be no notification for errors.
</pre>
## setup(opts) ## setup(opts)
`setup(opts)` \ `setup(opts)` \
Initialize oil Initialize oil
| Param | Type | Desc | | Param | Type | Desc |
| ----- | ------------ | ---- | | ----- | -------------------- | ---- |
| opts | `nil\|table` | | | opts | `oil.setupOpts\|nil` | |
<!-- /API --> <!-- /API -->

View file

@ -3,19 +3,21 @@
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
CONTENTS *oil-contents* CONTENTS *oil-contents*
1. Options |oil-options| 1. Config |oil-config|
2. Api |oil-api| 2. Options |oil-options|
3. Columns |oil-columns| 3. Api |oil-api|
4. Actions |oil-actions| 4. Columns |oil-columns|
5. Highlights |oil-highlights| 5. Actions |oil-actions|
6. Highlights |oil-highlights|
7. Trash |oil-trash|
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
OPTIONS *oil-options* CONFIG *oil-config*
>lua >lua
require("oil").setup({ require("oil").setup({
-- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`)
-- Set to false if you still want to use netrw. -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories.
default_file_explorer = true, default_file_explorer = true,
-- Id is automatically added at the beginning, and name at the end -- Id is automatically added at the beginning, and name at the end
-- See :help oil-columns -- See :help oil-columns
@ -43,16 +45,29 @@ OPTIONS *oil-option
}, },
-- Send deleted files to the trash instead of permanently deleting them (:help oil-trash) -- Send deleted files to the trash instead of permanently deleting them (:help oil-trash)
delete_to_trash = false, delete_to_trash = false,
-- Skip the confirmation popup for simple operations -- Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits)
skip_confirm_for_simple_edits = false, skip_confirm_for_simple_edits = false,
-- Change this to customize the command used when deleting to trash
trash_command = "trash-put",
-- Selecting a new/moved/renamed file or directory will prompt you to save changes first -- Selecting a new/moved/renamed file or directory will prompt you to save changes first
-- (:help prompt_save_on_select_new_entry)
prompt_save_on_select_new_entry = true, prompt_save_on_select_new_entry = true,
-- Oil will automatically delete hidden buffers after this delay -- Oil will automatically delete hidden buffers after this delay
-- You can set the delay to false to disable cleanup entirely -- You can set the delay to false to disable cleanup entirely
-- Note that the cleanup process only starts when none of the oil buffers are currently displayed -- Note that the cleanup process only starts when none of the oil buffers are currently displayed
cleanup_delay_ms = 2000, 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
-- Set to "unmodified" to only save unmodified buffers
autosave_changes = false,
},
-- Constrain the cursor to the editable parts of the oil buffer
-- Set to `false` to disable, or "name" to keep it on the file names
constrain_cursor = "editable",
-- Set to true to watch the filesystem for changes and reload oil
watch_for_changes = false,
-- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap
-- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" })
-- Additionally, if it is a string that matches "actions.<name>", -- Additionally, if it is a string that matches "actions.<name>",
@ -60,21 +75,22 @@ OPTIONS *oil-option
-- Set to `false` to remove a keymap -- Set to `false` to remove a keymap
-- See :help oil-actions for a list of all available actions -- See :help oil-actions for a list of all available actions
keymaps = { keymaps = {
["g?"] = "actions.show_help", ["g?"] = { "actions.show_help", mode = "n" },
["<CR>"] = "actions.select", ["<CR>"] = "actions.select",
["<C-s>"] = "actions.select_vsplit", ["<C-s>"] = { "actions.select", opts = { vertical = true } },
["<C-h>"] = "actions.select_split", ["<C-h>"] = { "actions.select", opts = { horizontal = true } },
["<C-t>"] = "actions.select_tab", ["<C-t>"] = { "actions.select", opts = { tab = true } },
["<C-p>"] = "actions.preview", ["<C-p>"] = "actions.preview",
["<C-c>"] = "actions.close", ["<C-c>"] = { "actions.close", mode = "n" },
["<C-l>"] = "actions.refresh", ["<C-l>"] = "actions.refresh",
["-"] = "actions.parent", ["-"] = { "actions.parent", mode = "n" },
["_"] = "actions.open_cwd", ["_"] = { "actions.open_cwd", mode = "n" },
["`"] = "actions.cd", ["`"] = { "actions.cd", mode = "n" },
["~"] = "actions.tcd", ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = "actions.change_sort", ["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external", ["gx"] = "actions.open_external",
["g."] = "actions.toggle_hidden", ["g."] = { "actions.toggle_hidden", mode = "n" },
["g\\"] = { "actions.toggle_trash", mode = "n" },
}, },
-- Set to false to disable all of the above keymaps -- Set to false to disable all of the above keymaps
use_default_keymaps = true, use_default_keymaps = true,
@ -83,37 +99,82 @@ OPTIONS *oil-option
show_hidden = false, show_hidden = false,
-- This function defines what is considered a "hidden" file -- This function defines what is considered a "hidden" file
is_hidden_file = function(name, bufnr) is_hidden_file = function(name, bufnr)
return vim.startswith(name, ".") local m = name:match("^%.")
return m ~= nil
end, end,
-- This function defines what will never be shown, even when `show_hidden` is set -- This function defines what will never be shown, even when `show_hidden` is set
is_always_hidden = function(name, bufnr) is_always_hidden = function(name, bufnr)
return false return false
end, 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 and directory names case insensitive
case_insensitive = false,
sort = { sort = {
-- sort order can be "asc" or "desc" -- sort order can be "asc" or "desc"
-- see :help oil-columns to see which columns are sortable -- see :help oil-columns to see which columns are sortable
{ "type", "asc" }, { "type", "asc" },
{ "name", "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
add = function(path)
return false
end,
mv = function(src_path, dest_path)
return false
end,
rm = function(path)
return false
end,
}, },
-- Configuration for the floating window in oil.open_float -- Configuration for the floating window in oil.open_float
float = { float = {
-- Padding around the floating window -- Padding around the floating window
padding = 2, 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_width = 0,
max_height = 0, max_height = 0,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, 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. -- This is the config that will be passed to nvim_open_win.
-- Change values here to customize the layout -- Change values here to customize the layout
override = function(conf) override = function(conf)
return conf return conf
end, end,
}, },
-- Configuration for the actions floating preview window -- Configuration for the file preview window
preview = { 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 = {
-- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- 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. -- 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" -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
@ -130,7 +191,7 @@ OPTIONS *oil-option
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window -- optionally define an integer/float for the exact height of the preview window
height = nil, height = nil,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -143,15 +204,56 @@ OPTIONS *oil-option
max_height = { 10, 0.9 }, max_height = { 10, 0.9 },
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
height = nil, height = nil,
border = "rounded", border = nil,
minimized_border = "none", minimized_border = "none",
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
}, },
-- Configuration for the floating SSH window
ssh = {
border = nil,
},
-- Configuration for the floating keymaps help window
keymaps_help = {
border = nil,
},
}) })
< <
--------------------------------------------------------------------------------
OPTIONS *oil-options*
skip_confirm_for_simple_edits *oil.skip_confirm_for_simple_edits*
type: `boolean` default: `false`
Before performing filesystem operations, Oil displays a confirmation popup to ensure
that all operations are intentional. When this option is `true`, the popup will be
skipped if the operations:
* contain no deletes
* contain no cross-adapter moves or copies (e.g. from local to ssh)
* contain at most one copy or move
* contain at most five creates
prompt_save_on_select_new_entry *oil.prompt_save_on_select_new_entry*
type: `boolean` default: `true`
There are two cases where this option is relevant:
1. You copy a file to a new location, then you select it and make edits before
saving.
2. You copy a directory to a new location, then you enter the directory and make
changes before saving.
In case 1, when you edit the file you are actually editing the original file because
oil has not yet moved/copied it to its new location. This means that the original
file will, perhaps unexpectedly, also be changed by any edits you make.
Case 2 is similar; when you edit the directory you are again actually editing the
original location of the directory. If you add new files, those files will be
created in both the original location and the copied directory.
When this option is `true`, Oil will prompt you to save before entering a file or
directory that is pending within oil, but does not exist on disk.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
API *oil-api* API *oil-api*
@ -180,77 +282,131 @@ set_sort({sort}) *oil.set_sor
Change the sort order for oil Change the sort order for oil
Parameters: Parameters:
{sort} `string[]` [] {sort} `oil.SortSpec[]` List of columns plus direction. See :help oil-
columns to see which ones are sortable.
Examples: >lua
require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } })
<
set_is_hidden_file({is_hidden_file}) *oil.set_is_hidden_file* set_is_hidden_file({is_hidden_file}) *oil.set_is_hidden_file*
Change how oil determines if the file is hidden Change how oil determines if the file is hidden
Parameters: Parameters:
{is_hidden_file} `fun(filename: string, bufnr: nil|integer): boolean` Retu {is_hidden_file} `fun(filename: string, bufnr: integer): boolean` Return
rn true if the file/dir should be hidden true if the file/dir should be hidden
toggle_hidden() *oil.toggle_hidden* toggle_hidden() *oil.toggle_hidden*
Toggle hidden files and directories Toggle hidden files and directories
get_current_dir(): nil|string *oil.get_current_dir* get_current_dir({bufnr}): nil|string *oil.get_current_dir*
Get the current directory Get the current directory
Parameters:
{bufnr} `nil|integer`
open_float({dir}) *oil.open_float* open_float({dir}, {opts}, {cb}) *oil.open_float*
Open oil browser in a floating window Open oil browser in a floating window
Parameters: Parameters:
{dir} `nil|string` When nil, open the parent of the current buffer, or the {dir} `nil|string` When nil, open the parent of the current buffer, or
cwd if current buffer is not a file 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
toggle_float({dir}) *oil.toggle_float* toggle_float({dir}, {opts}, {cb}) *oil.toggle_float*
Open oil browser in a floating window, or close it if open Open oil browser in a floating window, or close it if open
Parameters: Parameters:
{dir} `nil|string` When nil, open the parent of the current buffer, or the {dir} `nil|string` When nil, open the parent of the current buffer, or
cwd if current buffer is not a file 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
open({dir}) *oil.open* open({dir}, {opts}, {cb}) *oil.open*
Open oil browser for a directory Open oil browser for a directory
Parameters: Parameters:
{dir} `nil|string` When nil, open the parent of the current buffer, or the {dir} `nil|string` When nil, open the parent of the current buffer, or
cwd if current buffer is not a file 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
close() *oil.close* close({opts}) *oil.close*
Restore the buffer that was present when oil was opened 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*
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
modifier
{callback} `nil|fun(err: nil|string)` Called once the preview window has
been opened
select({opts}, {callback}) *oil.select* select({opts}, {callback}) *oil.select*
Select the entry under the cursor Select the entry under the cursor
Parameters: Parameters:
{opts} `nil|table` {opts} `nil|oil.SelectOpts`
{vertical} `boolean` Open the buffer in a vertical split {vertical} `nil|boolean` Open the buffer in a vertical split
{horizontal} `boolean` Open the buffer in a horizontal split {horizontal} `nil|boolean` Open the buffer in a horizontal split
{split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` Split
modifier modifier
{preview} `boolean` Open the buffer in a preview window {tab} `nil|boolean` Open the buffer in a new tab
{tab} `boolean` Open the buffer in a new tab {close} `nil|boolean` Close the original oil buffer once
{close} `boolean` Close the original oil buffer once selection is selection is made
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 {callback} `nil|fun(err: nil|string)` Called once all entries have been
opened opened
save({opts}) *oil.save* save({opts}, {cb}) *oil.save*
Save all changes Save all changes
Parameters: Parameters:
{opts} `nil|table` {opts} `nil|table`
{confirm} `nil|boolean` Show confirmation when true, never when false, {confirm} `nil|boolean` Show confirmation when true, never when false,
respect skip_confirm_for_simple_edits if nil respect skip_confirm_for_simple_edits if nil
{cb} `nil|fun(err: nil|string)` Called when mutations complete.
Note:
If you provide your own callback function, there will be no notification for errors.
setup({opts}) *oil.setup* setup({opts}) *oil.setup*
Initialize oil Initialize oil
Parameters: Parameters:
{opts} `nil|table` {opts} `oil.setupOpts|nil`
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
COLUMNS *oil-columns* COLUMNS *oil-columns*
@ -266,6 +422,7 @@ type *column-typ
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{icons} `table<string, string>` Mapping of entry type to icon {icons} `table<string, string>` Mapping of entry type to icon
icon *column-icon* icon *column-icon*
@ -275,6 +432,7 @@ icon *column-ico
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group 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 {default_file} `string` Fallback icon for files when nvim-web-devicons
returns nil returns nil
{directory} `string` Icon for directories {directory} `string` Icon for directories
@ -282,13 +440,14 @@ icon *column-ico
the icon the icon
size *column-size* size *column-size*
Adapters: files, ssh Adapters: files, ssh, s3
Sortable: this column can be used in view_props.sort Sortable: this column can be used in view_props.sort
The size of the file The size of the file
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
permissions *column-permissions* permissions *column-permissions*
Adapters: files, ssh Adapters: files, ssh
@ -298,6 +457,7 @@ permissions *column-permission
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
ctime *column-ctime* ctime *column-ctime*
Adapters: files Adapters: files
@ -307,6 +467,7 @@ ctime *column-ctim
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime) {format} `string` Format string (see :help strftime)
mtime *column-mtime* mtime *column-mtime*
@ -317,6 +478,7 @@ mtime *column-mtim
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime) {format} `string` Format string (see :help strftime)
atime *column-atime* atime *column-atime*
@ -327,40 +489,94 @@ atime *column-atim
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime) {format} `string` Format string (see :help strftime)
birthtime *column-birthtime* birthtime *column-birthtime*
Adapters: files Adapters: files, s3
Sortable: this column can be used in view_props.sort Sortable: this column can be used in view_props.sort
The time the file was created The time the file was created
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime) {format} `string` Format string (see :help strftime)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
ACTIONS *oil-actions* ACTIONS *oil-actions*
These are actions that can be used in the `keymaps` section of config options. The `keymaps` option in `oil.setup` allow you to create mappings using all the same parameters as |vim.keymap.set|.
>lua
keymaps = {
-- Mappings can be a string
["~"] = "<cmd>edit $HOME<CR>",
-- Mappings can be a function
["gd"] = function()
require("oil").set_columns({ "icon", "permissions", "size", "mtime" })
end,
-- You can pass additional opts to vim.keymap.set by using
-- a table with the mapping as the first element.
["<leader>ff"] = {
function()
require("telescope.builtin").find_files({
cwd = require("oil").get_current_dir()
})
end,
mode = "n",
nowait = true,
desc = "Find files in the current directory"
},
-- Mappings that are a string starting with "actions." will be
-- one of the built-in actions, documented below.
["`"] = "actions.tcd",
-- Some actions have parameters. These are passed in via the `opts` key.
["<leader>:"] = {
"actions.open_cmdline",
opts = {
shorten_path = true,
modify = ":h",
},
desc = "Open the command line with the current directory as an argument",
},
}
Below are the actions that can be used in the `keymaps` section of config
options. You can refer to them as strings (e.g. "actions.<action_name>") or you
can use the functions directly with
`require("oil.actions").action_name.callback()`
cd *actions.cd* cd *actions.cd*
:cd to the current oil directory :cd to the current oil directory
Parameters:
{scope} `nil|"tab"|"win"` Scope of the directory change (e.g. use |:tcd|
or |:lcd|)
{silent} `boolean` Do not show a message when changing directories
change_sort *actions.change_sort* change_sort *actions.change_sort*
Change the sort order Change the sort order
Parameters:
{sort} `oil.SortSpec[]` List of columns plus direction (see
|oil.set_sort|) instead of interactive selection
close *actions.close* close *actions.close*
Close oil and restore original buffer Close oil and restore original buffer
copy_entry_path *actions.copy_entry_path* Parameters:
Yank the filepath of the entry under the cursor to a register {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_cmdline *actions.open_cmdline*
Open vim cmdline with current entry as an argument Open vim cmdline with current entry as an argument
open_cmdline_dir *actions.open_cmdline_dir* Parameters:
Open vim cmdline with current directory as an argument {modify} `string` Modify the path with |fnamemodify()| using this as
the mods argument
{shorten_path} `boolean` Use relative paths when possible
open_cwd *actions.open_cwd* open_cwd *actions.open_cwd*
Open oil in Neovim's current working directory Open oil in Neovim's current working directory
@ -374,45 +590,95 @@ open_terminal *actions.open_termina
parent *actions.parent* parent *actions.parent*
Navigate to the parent path 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* preview *actions.preview*
Open the entry under the cursor in a preview window, or close the preview Open the entry under the cursor in a preview window, or close the preview
window if already open 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* preview_scroll_down *actions.preview_scroll_down*
Scroll down in the preview window 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* preview_scroll_up *actions.preview_scroll_up*
Scroll up in the preview window Scroll up in the preview window
refresh *actions.refresh* refresh *actions.refresh*
Refresh current directory list Refresh current directory list
Parameters:
{force} `boolean` When true, do not prompt user if they will be discarding
changes
select *actions.select* select *actions.select*
Open the entry under the cursor Open the entry under the cursor
select_split *actions.select_split* Parameters:
Open the entry under the cursor in a horizontal split {close} `boolean` Close the original oil buffer once selection is
made
{horizontal} `boolean` Open the buffer in a horizontal split
{split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split
modifier
{tab} `boolean` Open the buffer in a new tab
{vertical} `boolean` Open the buffer in a vertical split
select_tab *actions.select_tab* send_to_qflist *actions.send_to_qflist*
Open the entry under the cursor in a new tab Sends files in the current oil directory to the quickfix list, replacing the
previous entries.
select_vsplit *actions.select_vsplit* Parameters:
Open the entry under the cursor in a vertical split {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* show_help *actions.show_help*
Show default keymaps Show default keymaps
tcd *actions.tcd*
:tcd to the current oil directory
toggle_hidden *actions.toggle_hidden* toggle_hidden *actions.toggle_hidden*
Toggle hidden files and directories Toggle hidden files and directories
toggle_trash *actions.toggle_trash*
Jump to and from the trash for the current directory
yank_entry *actions.yank_entry*
Yank the filepath of the entry under the cursor to a register
Parameters:
{modify} `string` Modify the path with |fnamemodify()| using this as the
mods argument
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
HIGHLIGHTS *oil-highlights* HIGHLIGHTS *oil-highlights*
OilEmpty *hl-OilEmpty*
Empty column values
OilHidden *hl-OilHidden*
Hidden entry in an oil buffer
OilDir *hl-OilDir* OilDir *hl-OilDir*
Directories in an oil buffer Directory names in an oil buffer
OilDirHidden *hl-OilDirHidden*
Hidden directory names in an oil buffer
OilDirIcon *hl-OilDirIcon* OilDirIcon *hl-OilDirIcon*
Icon for directories Icon for directories
@ -420,12 +686,39 @@ OilDirIcon *hl-OilDirIco
OilSocket *hl-OilSocket* OilSocket *hl-OilSocket*
Socket files in an oil buffer Socket files in an oil buffer
OilSocketHidden *hl-OilSocketHidden*
Hidden socket files in an oil buffer
OilLink *hl-OilLink* OilLink *hl-OilLink*
Soft links in an oil buffer 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* OilFile *hl-OilFile*
Normal files in an oil buffer Normal files in an oil buffer
OilFileHidden *hl-OilFileHidden*
Hidden normal files in an oil buffer
OilCreate *hl-OilCreate* OilCreate *hl-OilCreate*
Create action in the oil preview window Create action in the oil preview window
@ -441,5 +734,45 @@ OilCopy *hl-OilCop
OilChange *hl-OilChange* OilChange *hl-OilChange*
Change action in the oil preview window Change action in the oil preview window
OilRestore *hl-OilRestore*
Restore (from the trash) action in the oil preview window
OilPurge *hl-OilPurge*
Purge (Permanently delete a file from trash) action in the oil preview
window
OilTrash *hl-OilTrash*
Trash (delete a file to trash) action in the oil preview window
OilTrashSourcePath *hl-OilTrashSourcePath*
Virtual text that shows the original path of file in the trash
--------------------------------------------------------------------------------
TRASH *oil-trash*
Oil has built-in support for using the system trash. When
`delete_to_trash = true`, any deleted files will be sent to the trash instead
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).
Linux:
Oil supports the FreeDesktop trash specification.
https://specifications.freedesktop.org/trash-spec/1.0/
All features should work.
Mac:
Oil has limited support for MacOS due to the proprietary nature of the
implementation. The trash bin can only be viewed as a single dir
(instead of being able to see files that were trashed from a directory).
Windows:
Oil supports the Windows Recycle Bin. All features should work.
================================================================================ ================================================================================
vim:tw=80:ts=2:ft=help:norl:syntax=help: vim:tw=80:ts=2:ft=help:norl:syntax=help:

127
doc/recipes.md Normal file
View file

@ -0,0 +1,127 @@
# Recipes
Have a cool recipe to share? Open a pull request and add it to this doc!
<!-- TOC -->
- [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)
<!-- /TOC -->
## Toggle file detail view
```lua
local detail = false
require("oil").setup({
keymaps = {
["gd"] = {
desc = "Toggle file detail view",
callback = function()
detail = not detail
if detail then
require("oil").set_columns({ "icon", "permissions", "size", "mtime" })
else
require("oil").set_columns({ "icon" })
end
end,
},
},
})
```
## 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
-- helper function to parse output
local function parse_output(proc)
local result = proc:wait()
local ret = {}
if result.code == 0 then
for line in vim.gsplit(result.stdout, "\n", { plain = true, trimempty = true }) do
-- Remove trailing slash
line = line:gsub("/$", "")
ret[line] = true
end
end
return ret
end
-- build git status cache
local function new_git_status()
return setmetatable({}, {
__index = function(self, key)
local ignore_proc = vim.system(
{ "git", "ls-files", "--ignored", "--exclude-standard", "--others", "--directory" },
{
cwd = key,
text = true,
}
)
local tracked_proc = vim.system({ "git", "ls-tree", "HEAD", "--name-only" }, {
cwd = key,
text = true,
})
local ret = {
ignored = parse_output(ignore_proc),
tracked = parse_output(tracked_proc),
}
rawset(self, key, ret)
return ret
end,
})
end
local git_status = new_git_status()
-- Clear git status cache on refresh
local refresh = require("oil.actions").refresh
local orig_refresh = refresh.callback
refresh.callback = function(...)
git_status = new_git_status()
orig_refresh(...)
end
require("oil").setup({
view_options = {
is_hidden_file = function(name, bufnr)
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
-- dotfiles are considered hidden unless tracked
if is_dotfile then
return not git_status[dir].tracked[name]
else
-- Check if file is gitignored
return git_status[dir].ignored[name]
end
end,
},
})
```

View file

@ -4,20 +4,48 @@ local util = require("oil.util")
local M = {} local M = {}
M.show_help = { M.show_help = {
desc = "Show default keymaps",
callback = function() callback = function()
local config = require("oil.config") local config = require("oil.config")
require("oil.keymap_util").show_help(config.keymaps) require("oil.keymap_util").show_help(config.keymaps)
end, end,
desc = "Show default keymaps",
} }
M.select = { M.select = {
desc = "Open the entry under the cursor", desc = "Open the entry under the cursor",
callback = oil.select, callback = function(opts)
opts = opts or {}
local callback = opts.callback
opts.callback = nil
oil.select(opts, callback)
end,
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",
},
tab = {
type = "boolean",
desc = "Open the buffer in a new tab",
},
close = {
type = "boolean",
desc = "Close the original oil buffer once selection is made",
},
},
} }
M.select_vsplit = { M.select_vsplit = {
desc = "Open the entry under the cursor in a vertical split", desc = "Open the entry under the cursor in a vertical split",
deprecated = true,
callback = function() callback = function()
oil.select({ vertical = true }) oil.select({ vertical = true })
end, end,
@ -25,6 +53,7 @@ M.select_vsplit = {
M.select_split = { M.select_split = {
desc = "Open the entry under the cursor in a horizontal split", desc = "Open the entry under the cursor in a horizontal split",
deprecated = true,
callback = function() callback = function()
oil.select({ horizontal = true }) oil.select({ horizontal = true })
end, end,
@ -32,6 +61,7 @@ M.select_split = {
M.select_tab = { M.select_tab = {
desc = "Open the entry under the cursor in a new tab", desc = "Open the entry under the cursor in a new tab",
deprecated = true,
callback = function() callback = function()
oil.select({ tab = true }) oil.select({ tab = true })
end, end,
@ -39,7 +69,21 @@ M.select_tab = {
M.preview = { M.preview = {
desc = "Open the entry under the cursor in a preview window, or close the preview window if already open", desc = "Open the entry under the cursor in a preview window, or close the preview window if already open",
callback = function() 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)
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
if not entry then if not entry then
vim.notify("Could not find entry under cursor", vim.log.levels.ERROR) vim.notify("Could not find entry under cursor", vim.log.levels.ERROR)
@ -50,10 +94,15 @@ M.preview = {
local cur_id = vim.w[winid].oil_entry_id local cur_id = vim.w[winid].oil_entry_id
if entry.id == cur_id then if entry.id == cur_id then
vim.api.nvim_win_close(winid, true) vim.api.nvim_win_close(winid, true)
if util.is_floating_win() then
local layout = require("oil.layout")
local win_opts = layout.get_fullscreen_win_opts()
vim.api.nvim_win_set_config(0, win_opts)
end
return return
end end
end end
oil.select({ preview = true }) oil.open_preview(opts)
end, end,
} }
@ -87,6 +136,30 @@ M.preview_scroll_up = {
end, 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 = { M.parent = {
desc = "Navigate to the parent path", desc = "Navigate to the parent path",
callback = oil.open, callback = oil.open,
@ -94,14 +167,27 @@ M.parent = {
M.close = { M.close = {
desc = "Close oil and restore original buffer", desc = "Close oil and restore original buffer",
callback = oil.close, 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",
},
},
} }
---@param cmd string ---@param cmd string
local function cd(cmd) ---@param silent? boolean
local function cd(cmd, silent)
local dir = oil.get_current_dir() local dir = oil.get_current_dir()
if dir then if dir then
vim.cmd({ cmd = cmd, args = { dir } }) vim.cmd({ cmd = cmd, args = { dir } })
if not silent then
vim.notify(string.format("CWD: %s", dir), vim.log.levels.INFO)
end
else else
vim.notify("Cannot :cd; not in a directory", vim.log.levels.WARN) vim.notify("Cannot :cd; not in a directory", vim.log.levels.WARN)
end end
@ -109,13 +195,31 @@ end
M.cd = { M.cd = {
desc = ":cd to the current oil directory", desc = ":cd to the current oil directory",
callback = function() callback = function(opts)
cd("cd") opts = opts or {}
local cmd = "cd"
if opts.scope == "tab" then
cmd = "tcd"
elseif opts.scope == "win" then
cmd = "lcd"
end
cd(cmd, opts.silent)
end, end,
parameters = {
scope = {
type = 'nil|"tab"|"win"',
desc = "Scope of the directory change (e.g. use |:tcd| or |:lcd|)",
},
silent = {
type = "boolean",
desc = "Do not show a message when changing directories",
},
},
} }
M.tcd = { M.tcd = {
desc = ":tcd to the current oil directory", desc = ":tcd to the current oil directory",
deprecated = true,
callback = function() callback = function()
cd("tcd") cd("tcd")
end, end,
@ -149,13 +253,24 @@ M.open_terminal = {
assert(dir, "Oil buffer with files adapter must have current directory") assert(dir, "Oil buffer with files adapter must have current directory")
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.fn.termopen(vim.o.shell, { cwd = dir }) 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
elseif adapter.name == "ssh" then elseif adapter.name == "ssh" then
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
local url = require("oil.adapters.ssh").parse_url(bufname) local url = require("oil.adapters.ssh").parse_url(bufname)
local cmd = require("oil.adapters.ssh.connection").create_ssh_command(url) local cmd = require("oil.adapters.ssh.connection").create_ssh_command(url)
local term_id = vim.fn.termopen(cmd) 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
if term_id then if term_id then
vim.api.nvim_chan_send(term_id, string.format("cd %s\n", url.path)) vim.api.nvim_chan_send(term_id, string.format("cd %s\n", url.path))
end end
@ -181,8 +296,8 @@ local function get_open_cmd(path)
else else
return nil, "rundll32 not found" return nil, "rundll32 not found"
end end
elseif vim.fn.executable("wslview") == 1 then elseif vim.fn.executable("explorer.exe") == 1 then
return { "wslview", path } return { "explorer.exe", path }
elseif vim.fn.executable("xdg-open") == 1 then elseif vim.fn.executable("xdg-open") == 1 then
return { "xdg-open", path } return { "xdg-open", path }
else else
@ -199,8 +314,12 @@ M.open_external = {
return return
end end
local path = dir .. entry.name local path = dir .. entry.name
-- TODO use vim.ui.open once this is resolved
-- https://github.com/neovim/neovim/issues/24567 if vim.ui.open then
vim.ui.open(path)
return
end
local cmd, err = get_open_cmd(path) local cmd, err = get_open_cmd(path)
if not cmd then if not cmd then
vim.notify(string.format("Could not open %s: %s", path, err), vim.log.levels.ERROR) vim.notify(string.format("Could not open %s: %s", path, err), vim.log.levels.ERROR)
@ -213,15 +332,25 @@ M.open_external = {
M.refresh = { M.refresh = {
desc = "Refresh current directory list", desc = "Refresh current directory list",
callback = function() callback = function(opts)
if vim.bo.modified then opts = opts or {}
if vim.bo.modified and not opts.force then
local ok, choice = pcall(vim.fn.confirm, "Discard changes?", "No\nYes") local ok, choice = pcall(vim.fn.confirm, "Discard changes?", "No\nYes")
if not ok or choice ~= 2 then if not ok or choice ~= 2 then
return return
end end
end end
vim.cmd.edit({ bang = true }) vim.cmd.edit({ bang = true })
-- :h CTRL-L-default
vim.cmd.nohlsearch()
end, end,
parameters = {
force = {
desc = "When true, do not prompt user if they will be discarding changes",
type = "boolean",
},
},
} }
local function open_cmdline_with_path(path) local function open_cmdline_with_path(path)
@ -232,7 +361,10 @@ end
M.open_cmdline = { M.open_cmdline = {
desc = "Open vim cmdline with current entry as an argument", desc = "Open vim cmdline with current entry as an argument",
callback = function() callback = function(opts)
opts = vim.tbl_deep_extend("keep", opts or {}, {
shorten_path = true,
})
local config = require("oil.config") local config = require("oil.config")
local fs = require("oil.fs") local fs = require("oil.fs")
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
@ -248,13 +380,57 @@ M.open_cmdline = {
if not adapter or not path or adapter.name ~= "files" then if not adapter or not path or adapter.name ~= "files" then
return return
end end
local fullpath = fs.shorten_path(fs.posix_to_os_path(path) .. entry.name) local fullpath = fs.posix_to_os_path(path) .. entry.name
if opts.modify then
fullpath = vim.fn.fnamemodify(fullpath, opts.modify)
end
if opts.shorten_path then
fullpath = fs.shorten_path(fullpath)
end
open_cmdline_with_path(fullpath) open_cmdline_with_path(fullpath)
end, end,
parameters = {
modify = {
desc = "Modify the path with |fnamemodify()| using this as the mods argument",
type = "string",
},
shorten_path = {
desc = "Use relative paths when possible",
type = "boolean",
},
},
}
M.yank_entry = {
desc = "Yank the filepath of the entry under the cursor to a register",
callback = function(opts)
opts = opts or {}
local entry = oil.get_cursor_entry()
local dir = oil.get_current_dir()
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
if opts.modify then
path = vim.fn.fnamemodify(path, opts.modify)
end
vim.fn.setreg(vim.v.register, path)
end,
parameters = {
modify = {
desc = "Modify the path with |fnamemodify()| using this as the mods argument",
type = "string",
},
},
} }
M.copy_entry_path = { M.copy_entry_path = {
desc = "Yank the filepath of the entry under the cursor to a register", desc = "Yank the filepath of the entry under the cursor to a register",
deprecated = true,
callback = function() callback = function()
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
local dir = oil.get_current_dir() local dir = oil.get_current_dir()
@ -265,8 +441,41 @@ M.copy_entry_path = {
end, end,
} }
M.copy_entry_filename = {
desc = "Yank the filename of the entry under the cursor to a register",
deprecated = true,
callback = function()
local entry = oil.get_cursor_entry()
if not entry then
return
end
vim.fn.setreg(vim.v.register, entry.name)
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 = { M.open_cmdline_dir = {
desc = "Open vim cmdline with current directory as an argument", desc = "Open vim cmdline with current directory as an argument",
deprecated = true,
callback = function() callback = function()
local fs = require("oil.fs") local fs = require("oil.fs")
local dir = oil.get_current_dir() local dir = oil.get_current_dir()
@ -278,7 +487,14 @@ M.open_cmdline_dir = {
M.change_sort = { M.change_sort = {
desc = "Change the sort order", desc = "Change the sort order",
callback = function() callback = function(opts)
opts = opts or {}
if opts.sort then
oil.set_sort(opts.sort)
return
end
local sort_cols = { "name", "size", "atime", "mtime", "ctime", "birthtime" } local sort_cols = { "name", "size", "atime", "mtime", "ctime", "birthtime" }
vim.ui.select(sort_cols, { prompt = "Sort by", kind = "oil_sort_col" }, function(col) vim.ui.select(sort_cols, { prompt = "Sort by", kind = "oil_sort_col" }, function(col)
if not col then if not col then
@ -300,6 +516,104 @@ M.change_sort = {
) )
end) end)
end, end,
parameters = {
sort = {
type = "oil.SortSpec[]",
desc = "List of columns plus direction (see |oil.set_sort|) instead of interactive selection",
},
},
}
M.toggle_trash = {
desc = "Jump to and from the trash for the current directory",
callback = function()
local fs = require("oil.fs")
local bufname = vim.api.nvim_buf_get_name(0)
local scheme, path = util.parse_url(bufname)
local bufnr = vim.api.nvim_get_current_buf()
local url
if scheme == "oil://" then
url = "oil-trash://" .. path
elseif scheme == "oil-trash://" then
url = "oil://" .. path
-- The non-linux trash implementations don't support per-directory trash,
-- so jump back to the stored source buffer.
if not fs.is_linux then
local src_bufnr = vim.b.oil_trash_toggle_src
if src_bufnr and vim.api.nvim_buf_is_valid(src_bufnr) then
url = vim.api.nvim_buf_get_name(src_bufnr)
end
end
else
vim.notify("No trash found for buffer", vim.log.levels.WARN)
return
end
vim.cmd.edit({ args = { url } })
vim.b.oil_trash_toggle_src = bufnr
end,
}
M.send_to_qflist = {
desc = "Sends files in the current oil directory to the quickfix list, replacing the previous entries.",
callback = function(opts)
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 = {
target = {
type = '"qflist"|"loclist"',
desc = "The target list to send files to",
},
action = {
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",
},
},
}
M.add_to_qflist = {
desc = "Adds files in the current oil directory to the quickfix list, keeping the previous entries.",
deprecated = true,
callback = function()
util.send_to_quickfix({
target = "qflist",
mode = "a",
})
end,
}
M.send_to_loclist = {
desc = "Sends files in the current oil directory to the location list, replacing the previous entries.",
deprecated = true,
callback = function()
util.send_to_quickfix({
target = "loclist",
mode = "r",
})
end,
}
M.add_to_loclist = {
desc = "Adds files in the current oil directory to the location list, keeping the previous entries.",
deprecated = true,
callback = function()
util.send_to_quickfix({
target = "loclist",
mode = "a",
})
end,
} }
---List actions for documentation generation ---List actions for documentation generation
@ -311,6 +625,8 @@ M._get_actions = function()
table.insert(ret, { table.insert(ret, {
name = name, name = name,
desc = action.desc, desc = action.desc,
deprecated = action.deprecated,
parameters = action.parameters,
}) })
end end
end end

View file

@ -3,13 +3,16 @@ local columns = require("oil.columns")
local config = require("oil.config") local config = require("oil.config")
local constants = require("oil.constants") local constants = require("oil.constants")
local fs = require("oil.fs") local fs = require("oil.fs")
local git = require("oil.git")
local log = require("oil.log")
local permissions = require("oil.adapters.files.permissions") local permissions = require("oil.adapters.files.permissions")
local trash = require("oil.adapters.files.trash")
local util = require("oil.util") local util = require("oil.util")
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
local M = {} local M = {}
local FIELD_NAME = constants.FIELD_NAME local FIELD_NAME = constants.FIELD_NAME
local FIELD_TYPE = constants.FIELD_TYPE
local FIELD_META = constants.FIELD_META local FIELD_META = constants.FIELD_META
local function read_link_data(path, cb) local function read_link_data(path, cb)
@ -32,34 +35,28 @@ local function read_link_data(path, cb)
) )
end 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 path string
---@param entry_type nil|oil.EntryType ---@param entry_type nil|oil.EntryType
---@return string ---@return string
M.to_short_os_path = function(path, entry_type) M.to_short_os_path = function(path, entry_type)
local shortpath = fs.shorten_path(fs.posix_to_os_path(path)) local shortpath = fs.shorten_path(fs.posix_to_os_path(path))
if entry_type == "directory" then if entry_type == "directory" then
shortpath = util.addslash(shortpath) shortpath = util.addslash(shortpath, true)
end end
return shortpath return shortpath
end end
local file_columns = {} 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)
uv.fs_stat(fs.join(dir, entry[FIELD_NAME]), cb)
end,
}
file_columns.size = { file_columns.size = {
meta_fields = fs_stat_meta_fields, require_stat = true,
render = function(entry, conf) render = function(entry, conf)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
local stat = meta.stat local stat = meta and meta.stat
if not stat then if not stat then
return columns.EMPTY return columns.EMPTY
end end
@ -76,7 +73,7 @@ file_columns.size = {
get_sort_value = function(entry) get_sort_value = function(entry)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
local stat = meta.stat local stat = meta and meta.stat
if stat then if stat then
return stat.size return stat.size
else else
@ -92,11 +89,11 @@ file_columns.size = {
-- TODO support file permissions on windows -- TODO support file permissions on windows
if not fs.is_windows then if not fs.is_windows then
file_columns.permissions = { file_columns.permissions = {
meta_fields = fs_stat_meta_fields, require_stat = true,
render = function(entry, conf) render = function(entry, conf)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
local stat = meta.stat local stat = meta and meta.stat
if not stat then if not stat then
return columns.EMPTY return columns.EMPTY
end end
@ -109,7 +106,7 @@ if not fs.is_windows then
compare = function(entry, parsed_value) compare = function(entry, parsed_value)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if parsed_value and meta.stat and meta.stat.mode then if parsed_value and meta and meta.stat and meta.stat.mode then
local mask = bit.lshift(1, 12) - 1 local mask = bit.lshift(1, 12) - 1
local old_mode = bit.band(meta.stat.mode, mask) local old_mode = bit.band(meta.stat.mode, mask)
if parsed_value ~= old_mode then if parsed_value ~= old_mode then
@ -147,15 +144,19 @@ if not fs.is_windows then
} }
end end
local current_year = vim.fn.strftime("%Y") local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function()
current_year = vim.fn.strftime("%Y")
end)
for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
file_columns[time_key] = { file_columns[time_key] = {
meta_fields = fs_stat_meta_fields, require_stat = true,
render = function(entry, conf) render = function(entry, conf)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
local stat = meta.stat local stat = meta and meta.stat
if not stat then if not stat then
return columns.EMPTY return columns.EMPTY
end end
@ -178,7 +179,20 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
local fmt = conf and conf.format local fmt = conf and conf.format
local pattern local pattern
if fmt then if fmt then
pattern = fmt:gsub("%%.", "%%S+") -- 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("%]", "%%]")
else else
pattern = "%S+%s+%d+%s+%d%d:?%d%d" pattern = "%S+%s+%d+%s+%d%d:?%d%d"
end end
@ -187,7 +201,7 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
get_sort_value = function(entry) get_sort_value = function(entry)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
local stat = meta.stat local stat = meta and meta.stat
if stat then if stat then
return stat[time_key].sec return stat[time_key].sec
else else
@ -197,6 +211,20 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
} }
end 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 ---@param name string
---@return nil|oil.ColumnDefinition ---@return nil|oil.ColumnDefinition
M.get_column = function(name) M.get_column = function(name)
@ -208,16 +236,36 @@ end
M.normalize_url = function(url, callback) M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url) local scheme, path = util.parse_url(url)
assert(path) assert(path)
if fs.is_windows then
if path == "/" then
return callback(url)
else
local is_root_drive = path:match("^/%u$")
if is_root_drive then
return callback(url .. "/")
end
end
end
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p") local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p")
uv.fs_realpath(os_path, function(err, new_os_path) uv.fs_realpath(os_path, function(err, new_os_path)
local realpath = new_os_path or os_path local realpath
if fs.is_windows then
-- Ignore the fs_realpath on windows because it will resolve mapped network drives to the IP
-- address instead of using the drive letter
realpath = os_path
else
realpath = new_os_path or os_path
end
uv.fs_stat( uv.fs_stat(
realpath, realpath,
vim.schedule_wrap(function(stat_err, stat) vim.schedule_wrap(function(stat_err, stat)
local is_directory local is_directory
if stat then if stat then
is_directory = stat.type == "directory" is_directory = stat.type == "directory"
elseif vim.endswith(realpath, "/") then elseif vim.endswith(realpath, "/") or (fs.is_windows and vim.endswith(realpath, "\\")) then
is_directory = true is_directory = true
else else
local filetype = vim.filetype.match({ filename = vim.fs.basename(realpath) }) local filetype = vim.filetype.match({ filename = vim.fs.basename(realpath) })
@ -228,7 +276,7 @@ M.normalize_url = function(url, callback)
local norm_path = util.addslash(fs.os_to_posix_path(realpath)) local norm_path = util.addslash(fs.os_to_posix_path(realpath))
callback(scheme .. norm_path) callback(scheme .. norm_path)
else else
callback(realpath) callback(vim.fn.fnamemodify(realpath, ":."))
end end
end) end)
) )
@ -254,16 +302,155 @@ M.get_entry_path = function(url, entry, cb)
end end
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 stdout = ""
local jid = vim.fn.jobstart({ "wmic", "logicaldisk", "get", "name" }, {
stdout_buffered = true,
on_stdout = function(_, data)
stdout = table.concat(data, "\n")
end,
on_exit = function(_, code)
if code ~= 0 then
return cb("Error listing windows devices")
end
local lines = vim.split(stdout, "\n", { plain = true, trimempty = true })
-- Remove the "Name" header
table.remove(lines, 1)
local internal_entries = {}
local complete_disk_cb = util.cb_collect(#lines, function(err)
if err then
cb(err)
else
cb(nil, internal_entries)
end
end)
for _, disk in ipairs(lines) do
if disk:match("^%s*$") then
-- Skip empty line
complete_disk_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)
end
end
end,
})
if jid <= 0 then
cb("Could not list windows devices")
end
end
---@param url string ---@param url string
---@param column_defs string[] ---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) ---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, cb) M.list = function(url, column_defs, cb)
local _, path = util.parse_url(url) local _, path = util.parse_url(url)
assert(path) assert(path)
if fs.is_windows and path == "/" then
return list_windows_drives(url, column_defs, cb)
end
local dir = fs.posix_to_os_path(path) local dir = fs.posix_to_os_path(path)
local fetch_meta = columns.get_metadata_fetcher(M, column_defs) local require_stat = columns_require_stat(column_defs)
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(dir, function(open_err, fd) uv.fs_opendir(dir, function(open_err, fd)
if open_err then if open_err then
if open_err:match("^ENOENT: no such file or directory") then if open_err:match("^ENOENT: no such file or directory") then
@ -293,32 +480,8 @@ M.list = function(url, column_defs, cb)
end) end)
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do
local cache_entry = cache.create_entry(url, entry.name, entry.type) local cache_entry = cache.create_entry(url, entry.name, entry.type)
fetch_meta(url, cache_entry, function(meta_err) table.insert(internal_entries, cache_entry)
if err then fetch_entry_metadata(path, cache_entry, require_stat, poll)
poll(meta_err)
else
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)
end end
else else
uv.fs_closedir(fd, function(close_err) uv.fs_closedir(fd, function(close_err)
@ -342,28 +505,17 @@ M.is_modifiable = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local _, path = util.parse_url(bufname) local _, path = util.parse_url(bufname)
assert(path) assert(path)
if fs.is_windows and path == "/" then
return false
end
local dir = fs.posix_to_os_path(path) local dir = fs.posix_to_os_path(path)
local stat = uv.fs_stat(dir) local stat = uv.fs_stat(dir)
if not stat then if not stat then
return true return true
end end
-- Can't do permissions checks on windows -- fs_access can return nil, force boolean return
if fs.is_windows then return uv.fs_access(dir, "W") == true
return true
end
local uid = uv.getuid()
local gid = uv.getgid()
local rwx
if uid == stat.uid then
rwx = bit.rshift(stat.mode, 6)
elseif gid == stat.gid then
rwx = bit.rshift(stat.mode, 3)
else
rwx = stat.mode
end
return bit.band(rwx, 2) ~= 0
end end
---@param action oil.Action ---@param action oil.Action
@ -380,9 +532,14 @@ M.render_action = function(action)
elseif action.type == "delete" then elseif action.type == "delete" then
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
return string.format("DELETE %s", M.to_short_os_path(path, action.entry_type)) local short_path = M.to_short_os_path(path, action.entry_type)
if config.delete_to_trash then
return string.format(" TRASH %s", short_path)
else
return string.format("DELETE %s", short_path)
end
elseif action.type == "move" or action.type == "copy" then elseif action.type == "move" or action.type == "copy" then
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url) local _, src_path = util.parse_url(action.src_url)
assert(src_path) assert(src_path)
@ -395,7 +552,7 @@ M.render_action = function(action)
M.to_short_os_path(dest_path, action.entry_type) M.to_short_os_path(dest_path, action.entry_type)
) )
else else
-- We should never hit this because we don't implement supported_adapters_for_copy -- We should never hit this because we don't implement supported_cross_adapter_actions
error("files adapter doesn't support cross-adapter move/copy") error("files adapter doesn't support cross-adapter move/copy")
end end
else else
@ -410,6 +567,18 @@ M.perform_action = function(action, cb)
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
path = fs.posix_to_os_path(path) path = fs.posix_to_os_path(path)
if config.git.add(path) then
local old_cb = cb
cb = vim.schedule_wrap(function(err)
if not err then
git.add(path, old_cb)
else
old_cb(err)
end
end)
end
if action.entry_type == "directory" then if action.entry_type == "directory" then
uv.fs_mkdir(path, 493, function(err) uv.fs_mkdir(path, 493, function(err)
-- Ignore if the directory already exists -- Ignore if the directory already exists
@ -437,13 +606,25 @@ M.perform_action = function(action, cb)
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
path = fs.posix_to_os_path(path) path = fs.posix_to_os_path(path)
if config.git.rm(path) then
local old_cb = cb
cb = vim.schedule_wrap(function(err)
if not err then
git.rm(path, old_cb)
else
old_cb(err)
end
end)
end
if config.delete_to_trash then if config.delete_to_trash then
trash.recursive_delete(path, cb) require("oil.adapters.trash").delete_to_trash(path, cb)
else else
fs.recursive_delete(action.entry_type, path, cb) fs.recursive_delete(action.entry_type, path, cb)
end end
elseif action.type == "move" then elseif action.type == "move" then
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url) local _, src_path = util.parse_url(action.src_url)
assert(src_path) assert(src_path)
@ -451,13 +632,17 @@ M.perform_action = function(action, cb)
assert(dest_path) assert(dest_path)
src_path = fs.posix_to_os_path(src_path) src_path = fs.posix_to_os_path(src_path)
dest_path = fs.posix_to_os_path(dest_path) dest_path = fs.posix_to_os_path(dest_path)
fs.recursive_move(action.entry_type, src_path, dest_path, vim.schedule_wrap(cb)) if config.git.mv(src_path, dest_path) then
git.mv(action.entry_type, src_path, dest_path, cb)
else
fs.recursive_move(action.entry_type, src_path, dest_path, cb)
end
else else
-- We should never hit this because we don't implement supported_adapters_for_copy -- We should never hit this because we don't implement supported_cross_adapter_actions
cb("files adapter doesn't support cross-adapter move") cb("files adapter doesn't support cross-adapter move")
end end
elseif action.type == "copy" then elseif action.type == "copy" then
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url) local _, src_path = util.parse_url(action.src_url)
assert(src_path) assert(src_path)
@ -467,7 +652,7 @@ M.perform_action = function(action, cb)
dest_path = fs.posix_to_os_path(dest_path) dest_path = fs.posix_to_os_path(dest_path)
fs.recursive_copy(action.entry_type, src_path, dest_path, cb) fs.recursive_copy(action.entry_type, src_path, dest_path, cb)
else else
-- We should never hit this because we don't implement supported_adapters_for_copy -- We should never hit this because we don't implement supported_cross_adapter_actions
cb("files adapter doesn't support cross-adapter copy") cb("files adapter doesn't support cross-adapter copy")
end end
else else

View file

@ -1,6 +1,6 @@
local M = {} local M = {}
---@param exe_modifier nil|false|string ---@param exe_modifier false|string
---@param num integer ---@param num integer
---@return string ---@return string
local function perm_to_str(exe_modifier, num) local function perm_to_str(exe_modifier, num)

View file

@ -1,44 +0,0 @@
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

389
lua/oil/adapters/s3.lua Normal file
View file

@ -0,0 +1,389 @@
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

View file

@ -0,0 +1,149 @@
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

View file

@ -20,6 +20,13 @@ local FIELD_META = constants.FIELD_META
---@field port nil|integer ---@field port nil|integer
---@field path string ---@field path string
---@param args string[]
local function scp(args, ...)
local cmd = vim.list_extend({ "scp", "-C" }, config.extra_scp_args)
vim.list_extend(cmd, args)
shell.run(cmd, ...)
end
---@param oil_url string ---@param oil_url string
---@return oil.sshUrl ---@return oil.sshUrl
M.parse_url = function(oil_url) M.parse_url = function(oil_url)
@ -43,6 +50,7 @@ M.parse_url = function(oil_url)
error(string.format("Malformed SSH url: %s", oil_url)) error(string.format("Malformed SSH url: %s", oil_url))
end end
---@cast ret oil.sshUrl
return ret return ret
end end
@ -109,7 +117,7 @@ local ssh_columns = {}
ssh_columns.permissions = { ssh_columns.permissions = {
render = function(entry, conf) render = function(entry, conf)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
return permissions.mode_to_str(meta.mode) return meta and permissions.mode_to_str(meta.mode)
end, end,
parse = function(line, conf) parse = function(line, conf)
@ -118,7 +126,7 @@ ssh_columns.permissions = {
compare = function(entry, parsed_value) compare = function(entry, parsed_value)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if parsed_value and meta.mode then if parsed_value and meta and meta.mode then
local mask = bit.lshift(1, 12) - 1 local mask = bit.lshift(1, 12) - 1
local old_mode = bit.band(meta.mode, mask) local old_mode = bit.band(meta.mode, mask)
if parsed_value ~= old_mode then if parsed_value ~= old_mode then
@ -142,7 +150,7 @@ ssh_columns.permissions = {
ssh_columns.size = { ssh_columns.size = {
render = function(entry, conf) render = function(entry, conf)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if not meta.size then if not meta or not meta.size then
return "" return ""
elseif meta.size >= 1e9 then elseif meta.size >= 1e9 then
return string.format("%.1fG", meta.size / 1e9) return string.format("%.1fG", meta.size / 1e9)
@ -161,7 +169,7 @@ ssh_columns.size = {
get_sort_value = function(entry) get_sort_value = function(entry)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if meta.size then if meta and meta.size then
return meta.size return meta.size
else else
return 0 return 0
@ -295,15 +303,15 @@ M.perform_action = function(action, cb)
local conn = get_connection(action.url) local conn = get_connection(action.url)
conn:rm(res.path, cb) conn:rm(res.path, cb)
elseif action.type == "move" then elseif action.type == "move" then
local src_adapter = config.get_adapter_by_scheme(action.src_url) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter == M and dest_adapter == M then if src_adapter == M and dest_adapter == M then
local src_res = M.parse_url(action.src_url) local src_res = M.parse_url(action.src_url)
local dest_res = M.parse_url(action.dest_url) local dest_res = M.parse_url(action.dest_url)
local src_conn = get_connection(action.src_url) local src_conn = get_connection(action.src_url)
local dest_conn = get_connection(action.dest_url) local dest_conn = get_connection(action.dest_url)
if src_conn ~= dest_conn then if src_conn ~= dest_conn then
shell.run({ "scp", "-C", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err) scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err)
if err then if err then
return cb(err) return cb(err)
end end
@ -316,13 +324,13 @@ M.perform_action = function(action, cb)
cb("We should never attempt to move across adapters") cb("We should never attempt to move across adapters")
end end
elseif action.type == "copy" then elseif action.type == "copy" then
local src_adapter = config.get_adapter_by_scheme(action.src_url) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter == M and dest_adapter == M then if src_adapter == M and dest_adapter == M then
local src_res = M.parse_url(action.src_url) local src_res = M.parse_url(action.src_url)
local dest_res = M.parse_url(action.dest_url) local dest_res = M.parse_url(action.dest_url)
if not url_hosts_equal(src_res, dest_res) then if not url_hosts_equal(src_res, dest_res) then
shell.run({ "scp", "-C", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb) scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb)
else else
local src_conn = get_connection(action.src_url) local src_conn = get_connection(action.src_url)
src_conn:cp(src_res.path, dest_res.path, cb) src_conn:cp(src_res.path, dest_res.path, cb)
@ -341,14 +349,14 @@ M.perform_action = function(action, cb)
src_arg = fs.posix_to_os_path(path) src_arg = fs.posix_to_os_path(path)
dest_arg = url_to_scp(M.parse_url(action.dest_url)) dest_arg = url_to_scp(M.parse_url(action.dest_url))
end end
shell.run({ "scp", "-C", "-r", src_arg, dest_arg }, cb) scp({ "-r", src_arg, dest_arg }, cb)
end end
else else
cb(string.format("Bad action type: %s", action.type)) cb(string.format("Bad action type: %s", action.type))
end end
end end
M.supported_adapters_for_copy = { files = true } M.supported_cross_adapter_actions = { files = "copy" }
---@param bufnr integer ---@param bufnr integer
M.read_file = function(bufnr) M.read_file = function(bufnr)
@ -357,7 +365,9 @@ M.read_file = function(bufnr)
local url = M.parse_url(bufname) local url = M.parse_url(bufname)
local scp_url = url_to_scp(url) local scp_url = url_to_scp(url)
local basename = pathutil.basename(bufname) local basename = pathutil.basename(bufname)
local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil") local cache_dir = vim.fn.stdpath("cache")
assert(type(cache_dir) == "string")
local tmpdir = fs.join(cache_dir, "oil")
fs.mkdirp(tmpdir) fs.mkdirp(tmpdir)
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXX")) local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXX"))
if fd then if fd then
@ -365,7 +375,7 @@ M.read_file = function(bufnr)
end end
local tmp_bufnr = vim.fn.bufadd(tmpfile) local tmp_bufnr = vim.fn.bufadd(tmpfile)
shell.run({ "scp", "-C", scp_url, tmpfile }, function(err) scp({ scp_url, tmpfile }, function(err)
loading.set_loading(bufnr, false) loading.set_loading(bufnr, false)
vim.bo[bufnr].modifiable = true vim.bo[bufnr].modifiable = true
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } })
@ -395,7 +405,9 @@ M.write_file = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local url = M.parse_url(bufname) local url = M.parse_url(bufname)
local scp_url = url_to_scp(url) local scp_url = url_to_scp(url)
local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil") 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, "ssh_XXXXXXXX")) local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXXXX"))
if fd then if fd then
vim.loop.fs_close(fd) vim.loop.fs_close(fd)
@ -405,7 +417,7 @@ M.write_file = function(bufnr)
vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } }) vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } })
local tmp_bufnr = vim.fn.bufadd(tmpfile) local tmp_bufnr = vim.fn.bufadd(tmpfile)
shell.run({ "scp", "-C", tmpfile, scp_url }, function(err) scp({ tmpfile, scp_url }, function(err)
vim.bo[bufnr].modifiable = true vim.bo[bufnr].modifiable = true
if err then if err then
vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR) vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR)
@ -429,6 +441,7 @@ M.goto_file = function()
url.path = vim.fs.dirname(fullpath) url.path = vim.fs.dirname(fullpath)
local parurl = url_to_str(url) local parurl = url_to_str(url)
---@cast M oil.Adapter
util.adapter_list_all(M, parurl, {}, function(err, entries) util.adapter_list_all(M, parurl, {}, function(err, entries)
if err then if err then
vim.notify(string.format("Error finding file '%s': %s", fname, err), vim.log.levels.ERROR) vim.notify(string.format("Error finding file '%s': %s", fname, err), vim.log.levels.ERROR)

View file

@ -1,3 +1,4 @@
local config = require("oil.config")
local layout = require("oil.layout") local layout = require("oil.layout")
local util = require("oil.util") local util = require("oil.util")
@ -155,7 +156,7 @@ function SSHConnection.new(url)
else else
self.jid = jid self.jid = jid
end end
self:run("whoami", function(err, lines) self:run("id -u", function(err, lines)
if err then if err then
vim.notify(string.format("Error fetching ssh connection user: %s", err), vim.log.levels.WARN) vim.notify(string.format("Error fetching ssh connection user: %s", err), vim.log.levels.WARN)
else else
@ -163,7 +164,7 @@ function SSHConnection.new(url)
self.meta.user = vim.trim(table.concat(lines, "")) self.meta.user = vim.trim(table.concat(lines, ""))
end end
end) end)
self:run("groups", function(err, lines) self:run("id -G", function(err, lines)
if err then if err then
vim.notify( vim.notify(
string.format("Error fetching ssh connection user groups: %s", err), string.format("Error fetching ssh connection user groups: %s", err),
@ -175,6 +176,7 @@ function SSHConnection.new(url)
end end
end) end)
---@cast self oil.sshConnection
return self return self
end end
@ -277,7 +279,7 @@ function SSHConnection:open_terminal()
row = row, row = row,
col = col, col = col,
style = "minimal", style = "minimal",
border = "rounded", border = config.ssh.border,
}) })
vim.cmd.startinsert() vim.cmd.startinsert()
end end

View file

@ -27,7 +27,7 @@ local typechar_map = {
---@return table Metadata for entry ---@return table Metadata for entry
local function parse_ls_line(line) local function parse_ls_line(line)
local typechar, perms, refcount, user, group, rem = local typechar, perms, refcount, user, group, rem =
line:match("^(.)(%S+)%s+(%d+)%s+(%S+)%s+(%S+)%s+(.*)$") line:match("^(.)(%S+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(.*)$")
if not typechar then if not typechar then
error(string.format("Could not parse '%s'", line)) error(string.format("Could not parse '%s'", line))
end end
@ -42,10 +42,17 @@ local function parse_ls_line(line)
local name, size, date, major, minor local name, size, date, major, minor
if typechar == "c" or typechar == "b" then 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+(.*)") 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.major = tonumber(major)
meta.minor = tonumber(minor) meta.minor = tonumber(minor)
else else
size, date, name = rem:match("^(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)") 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) meta.size = tonumber(size)
end end
meta.iso_modified_date = date meta.iso_modified_date = date
@ -61,9 +68,16 @@ local function parse_ls_line(line)
return name, type, meta return name, type, meta
end end
---@param str string String to escape
---@return string Escaped string
local function shellescape(str)
return "'" .. str:gsub("'", "'\\''") .. "'"
end
---@param url oil.sshUrl ---@param url oil.sshUrl
---@return oil.sshFs ---@return oil.sshFs
function SSHFS.new(url) function SSHFS.new(url)
---@type oil.sshFs
return setmetatable({ return setmetatable({
conn = SSHConnection.new(url), conn = SSHConnection.new(url),
}, { }, {
@ -80,7 +94,7 @@ end
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:chmod(value, path, callback) function SSHFS:chmod(value, path, callback)
local octal = permissions.mode_to_octal_str(value) local octal = permissions.mode_to_octal_str(value)
self.conn:run(string.format("chmod %s '%s'", octal, path), callback) self.conn:run(string.format("chmod %s %s", octal, shellescape(path)), callback)
end end
function SSHFS:open_terminal() function SSHFS:open_terminal()
@ -105,21 +119,24 @@ function SSHFS:realpath(path, callback)
if vim.endswith(abspath, ".") then if vim.endswith(abspath, ".") then
abspath = abspath:sub(1, #abspath - 1) abspath = abspath:sub(1, #abspath - 1)
end end
self.conn:run(string.format("ls -ald --color=never '%s'", abspath), function(ls_err, ls_lines) self.conn:run(
local type string.format("LC_ALL=C ls -land --color=never %s", shellescape(abspath)),
if ls_err then function(ls_err, ls_lines)
-- If the file doesn't exist, treat it like a not-yet-existing directory local type
type = "directory" if ls_err then
else -- If the file doesn't exist, treat it like a not-yet-existing directory
assert(ls_lines) type = "directory"
local _ else
_, type = parse_ls_line(ls_lines[1]) assert(ls_lines)
local _
_, type = parse_ls_line(ls_lines[1])
end
if type == "directory" then
abspath = util.addslash(abspath)
end
callback(nil, abspath)
end end
if type == "directory" then )
abspath = util.addslash(abspath)
end
callback(nil, abspath)
end)
end) end)
end end
@ -131,9 +148,9 @@ local dir_meta = {}
function SSHFS:list_dir(url, path, callback) function SSHFS:list_dir(url, path, callback)
local path_postfix = "" local path_postfix = ""
if path ~= "" then if path ~= "" then
path_postfix = string.format(" '%s'", path) path_postfix = string.format(" %s", shellescape(path))
end end
self.conn:run("LANG=C ls -al --color=never" .. path_postfix, function(err, lines) self.conn:run("LC_ALL=C ls -lan --color=never" .. path_postfix, function(err, lines)
if err then if err then
if err:match("No such file or directory%s*$") then if err:match("No such file or directory%s*$") then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse -- If the directory doesn't exist, treat the list as a success. We will be able to traverse
@ -166,28 +183,31 @@ function SSHFS:list_dir(url, path, callback)
if any_links then if any_links then
-- If there were any soft links, then we need to run another ls command with -L so that we can -- If there were any soft links, then we need to run another ls command with -L so that we can
-- resolve the type of the link target -- resolve the type of the link target
self.conn:run("ls -aLl --color=never" .. path_postfix, function(link_err, link_lines) self.conn:run(
-- Ignore exit code 1. That just means one of the links could not be resolved. "LC_ALL=C ls -naLl --color=never" .. path_postfix .. " 2> /dev/null",
if link_err and not link_err:match("^1:") then function(link_err, link_lines)
return callback(link_err) -- Ignore exit code 1. That just means one of the links could not be resolved.
end if link_err and not link_err:match("^1:") then
assert(link_lines) return callback(link_err)
for _, line in ipairs(link_lines) do end
if line ~= "" and not line:match("^total") then assert(link_lines)
local ok, name, type, meta = pcall(parse_ls_line, line) for _, line in ipairs(link_lines) do
if ok and name ~= "." and name ~= ".." then if line ~= "" and not line:match("^total") then
local cache_entry = entries[name] local ok, name, type, meta = pcall(parse_ls_line, line)
if cache_entry[FIELD_TYPE] == "link" then if ok and name ~= "." and name ~= ".." then
cache_entry[FIELD_META].link_stat = { local cache_entry = entries[name]
type = type, if cache_entry[FIELD_TYPE] == "link" then
size = meta.size, cache_entry[FIELD_META].link_stat = {
} type = type,
size = meta.size,
}
end
end end
end end
end end
callback(nil, cache_entries)
end end
callback(nil, cache_entries) )
end)
else else
callback(nil, cache_entries) callback(nil, cache_entries)
end end
@ -197,40 +217,40 @@ end
---@param path string ---@param path string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:mkdir(path, callback) function SSHFS:mkdir(path, callback)
self.conn:run(string.format("mkdir -p '%s'", path), callback) self.conn:run(string.format("mkdir -p %s", shellescape(path)), callback)
end end
---@param path string ---@param path string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:touch(path, callback) function SSHFS:touch(path, callback)
self.conn:run(string.format("touch '%s'", path), callback) self.conn:run(string.format("touch %s", shellescape(path)), callback)
end end
---@param path string ---@param path string
---@param link string ---@param link string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:mklink(path, link, callback) function SSHFS:mklink(path, link, callback)
self.conn:run(string.format("ln -s '%s' '%s'", link, path), callback) self.conn:run(string.format("ln -s %s %s", shellescape(link), shellescape(path)), callback)
end end
---@param path string ---@param path string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:rm(path, callback) function SSHFS:rm(path, callback)
self.conn:run(string.format("rm -rf '%s'", path), callback) self.conn:run(string.format("rm -rf %s", shellescape(path)), callback)
end end
---@param src string ---@param src string
---@param dest string ---@param dest string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:mv(src, dest, callback) function SSHFS:mv(src, dest, callback)
self.conn:run(string.format("mv '%s' '%s'", src, dest), callback) self.conn:run(string.format("mv %s %s", shellescape(src), shellescape(dest)), callback)
end end
---@param src string ---@param src string
---@param dest string ---@param dest string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:cp(src, dest, callback) function SSHFS:cp(src, dest, callback)
self.conn:run(string.format("cp -r '%s' '%s'", src, dest), callback) self.conn:run(string.format("cp -r %s %s", shellescape(src), shellescape(dest)), callback)
end end
function SSHFS:get_dir_meta(url) function SSHFS:get_dir_meta(url)

View file

@ -0,0 +1,9 @@
local fs = require("oil.fs")
if fs.is_mac then
return require("oil.adapters.trash.mac")
elseif fs.is_windows then
return require("oil.adapters.trash.windows")
else
return require("oil.adapters.trash.freedesktop")
end

View file

@ -0,0 +1,633 @@
-- Based on the FreeDesktop.org trash specification
-- https://specifications.freedesktop.org/trash-spec/1.0/
local cache = require("oil.cache")
local config = require("oil.config")
local constants = require("oil.constants")
local files = require("oil.adapters.files")
local fs = require("oil.fs")
local util = require("oil.util")
local uv = vim.uv or vim.loop
local FIELD_META = constants.FIELD_META
local M = {}
local function ensure_trash_dir(path)
local mode = 448 -- 0700
fs.mkdirp(fs.join(path, "info"), mode)
fs.mkdirp(fs.join(path, "files"), mode)
end
---Gets the location of the home trash dir, creating it if necessary
---@return string
local function get_home_trash_dir()
local xdg_home = vim.env.XDG_DATA_HOME
if not xdg_home then
xdg_home = fs.join(assert(uv.os_homedir()), ".local", "share")
end
local trash_dir = fs.join(xdg_home, "Trash")
ensure_trash_dir(trash_dir)
return trash_dir
end
---@param mode integer
---@return boolean
local function is_sticky(mode)
local extra = bit.rshift(mode, 9)
return bit.band(extra, 4) ~= 0
end
---Get the topdir .Trash/$uid directory if present and valid
---@param path string
---@return string[]
local function get_top_trash_dirs(path)
local dirs = {}
local dev = (uv.fs_lstat(path) or {}).dev
local top_trash_dirs = vim.fs.find(".Trash", { upward = true, path = path, limit = math.huge })
for _, top_trash_dir in ipairs(top_trash_dirs) do
local stat = uv.fs_lstat(top_trash_dir)
if stat and not dev then
dev = stat.dev
end
if stat and stat.dev == dev and stat.type == "directory" and is_sticky(stat.mode) then
local trash_dir = fs.join(top_trash_dir, tostring(uv.getuid()))
ensure_trash_dir(trash_dir)
table.insert(dirs, trash_dir)
end
end
-- Also search for the .Trash-$uid
top_trash_dirs = vim.fs.find(
string.format(".Trash-%d", uv.getuid()),
{ upward = true, path = path, limit = math.huge }
)
for _, top_trash_dir in ipairs(top_trash_dirs) do
local stat = uv.fs_lstat(top_trash_dir)
if stat and stat.dev == dev then
ensure_trash_dir(top_trash_dir)
table.insert(dirs, top_trash_dir)
end
end
return dirs
end
---@param path string
---@return string
local function get_write_trash_dir(path)
local lstat = uv.fs_lstat(path)
local home_trash = get_home_trash_dir()
if not lstat then
-- If the source file doesn't exist default to home trash dir
return home_trash
end
local dev = lstat.dev
if uv.fs_lstat(home_trash).dev == dev then
return home_trash
end
local top_trash_dirs = get_top_trash_dirs(path)
if not vim.tbl_isempty(top_trash_dirs) then
return top_trash_dirs[1]
end
local parent = vim.fn.fnamemodify(path, ":h")
local next_parent = vim.fn.fnamemodify(parent, ":h")
while parent ~= next_parent and uv.fs_lstat(next_parent).dev == dev do
parent = next_parent
next_parent = vim.fn.fnamemodify(parent, ":h")
end
local top_trash = fs.join(parent, string.format(".Trash-%d", uv.getuid()))
ensure_trash_dir(top_trash)
return top_trash
end
---@param path string
---@return string[]
local function get_read_trash_dirs(path)
local dirs = { get_home_trash_dir() }
vim.list_extend(dirs, get_top_trash_dirs(path))
return dirs
end
---@param url string
---@param callback fun(url: string)
M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url)
assert(path)
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p")
uv.fs_realpath(
os_path,
vim.schedule_wrap(function(err, new_os_path)
local realpath = new_os_path or os_path
callback(scheme .. util.addslash(fs.os_to_posix_path(realpath)))
end)
)
end
---@param url string
---@param entry oil.Entry
---@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])
---@type oil.TrashInfo
local trash_info = meta.trash_info
if not trash_info then
-- This is a subpath in the trash
M.normalize_url(url, cb)
return
end
local path = fs.os_to_posix_path(trash_info.trash_file)
if meta.stat.type == "directory" then
path = util.addslash(path)
end
cb("oil://" .. path)
end
---@class oil.TrashInfo
---@field trash_file string
---@field info_file string
---@field original_path string
---@field deletion_date number
---@field stat uv.aliases.fs_stat_table
---@param info_file string
---@param cb fun(err?: string, info?: oil.TrashInfo)
local function read_trash_info(info_file, cb)
if not vim.endswith(info_file, ".trashinfo") then
return cb("File is not .trashinfo")
end
uv.fs_open(info_file, "r", 448, function(err, fd)
if err then
return cb(err)
end
assert(fd)
uv.fs_fstat(fd, function(stat_err, stat)
if stat_err then
uv.fs_close(fd)
return cb(stat_err)
end
uv.fs_read(
fd,
assert(stat).size,
nil,
vim.schedule_wrap(function(read_err, content)
uv.fs_close(fd)
if read_err then
return cb(read_err)
end
assert(content)
local trash_info = {
info_file = info_file,
}
local lines = vim.split(content, "\r?\n")
if lines[1] ~= "[Trash Info]" then
return cb("File missing [Trash Info] header")
end
local trash_base = vim.fn.fnamemodify(info_file, ":h:h")
for _, line in ipairs(lines) do
local key, value = unpack(vim.split(line, "=", { plain = true, trimempty = true }))
if key == "Path" and not trash_info.original_path then
if not vim.startswith(value, "/") then
value = fs.join(trash_base, value)
end
trash_info.original_path = value
elseif key == "DeletionDate" and not trash_info.deletion_date then
trash_info.deletion_date = vim.fn.strptime("%Y-%m-%dT%H:%M:%S", value)
end
end
if not trash_info.original_path or not trash_info.deletion_date then
return cb("File missing required fields")
end
local basename = vim.fn.fnamemodify(info_file, ":t:r")
trash_info.trash_file = fs.join(trash_base, "files", basename)
uv.fs_lstat(trash_info.trash_file, function(trash_stat_err, trash_stat)
if trash_stat_err then
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)
end)
)
end)
end)
end
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, cb)
cb = vim.schedule_wrap(cb)
local _, path = util.parse_url(url)
assert(path)
local trash_dirs = get_read_trash_dirs(path)
local trash_idx = 0
local read_next_trash_dir
read_next_trash_dir = function()
trash_idx = trash_idx + 1
local trash_dir = trash_dirs[trash_idx]
if not trash_dir then
return cb()
end
-- Show all files from the trash directory if we are in the root of the device, which we can
-- tell if the trash dir is a subpath of our current path
local show_all_files = fs.is_subpath(path, trash_dir)
-- The first trash dir is a special case; it is in the home directory and we should only show
-- all entries if we are in the top root path "/"
if trash_idx == 1 then
show_all_files = path == "/"
end
local info_dir = fs.join(trash_dir, "info")
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(info_dir, function(open_err, fd)
if open_err then
if open_err:match("^ENOENT: no such file or directory") then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse
-- and edit a not-yet-existing directory.
return read_next_trash_dir()
else
return cb(open_err)
end
end
local read_next
read_next = function()
uv.fs_readdir(fd, function(err, entries)
if err then
uv.fs_closedir(fd, function()
cb(err)
end)
return
elseif entries then
local internal_entries = {}
local poll = util.cb_collect(#entries, function(inner_err)
if inner_err then
cb(inner_err)
else
cb(nil, internal_entries, read_next)
end
end)
for _, entry in ipairs(entries) do
read_trash_info(
fs.join(info_dir, entry.name),
vim.schedule_wrap(function(read_err, info)
if read_err then
-- Discard the error. We don't care if there's something wrong with one of these
-- files.
poll()
else
local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ":h"))
if path == parent or show_all_files then
local name = vim.fn.fnamemodify(info.trash_file, ":t")
---@diagnostic disable-next-line: undefined-field
local cache_entry = cache.create_entry(url, name, info.stat.type)
local display_name = vim.fn.fnamemodify(info.original_path, ":t")
cache_entry[FIELD_META] = {
stat = info.stat,
trash_info = info,
display_name = display_name,
}
table.insert(internal_entries, cache_entry)
end
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name)
while next_par ~= "." do
name = next_par
next_par = vim.fs.dirname(name)
end
---@diagnostic disable-next-line: undefined-field
local cache_entry = cache.create_entry(url, name, "directory")
cache_entry[FIELD_META] = {
stat = info.stat,
}
table.insert(internal_entries, cache_entry)
end
poll()
end
end)
)
end
else
uv.fs_closedir(fd, function(close_err)
if close_err then
cb(close_err)
else
vim.schedule(read_next_trash_dir)
end
end)
end
end)
end
read_next()
---@diagnostic disable-next-line: param-type-mismatch
end, 10000)
end
read_next_trash_dir()
end
---@param bufnr integer
---@return boolean
M.is_modifiable = function(bufnr)
return true
end
local file_columns = {}
local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function()
current_year = vim.fn.strftime("%Y")
end)
file_columns.mtime = {
render = function(entry, conf)
local meta = entry[FIELD_META]
if not meta then
return nil
end
---@type oil.TrashInfo
local trash_info = meta.trash_info
local time = trash_info and trash_info.deletion_date or meta.stat and meta.stat.mtime.sec
if not time then
return nil
end
local fmt = conf and conf.format
local ret
if fmt then
ret = vim.fn.strftime(fmt, time)
else
local year = vim.fn.strftime("%Y", time)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", time)
else
ret = vim.fn.strftime("%b %d %H:%M", time)
end
end
return ret
end,
get_sort_value = function(entry)
local meta = entry[FIELD_META]
---@type nil|oil.TrashInfo
local trash_info = meta and meta.trash_info
if trash_info then
return trash_info.deletion_date
else
return 0
end
end,
parse = function(line, conf)
local fmt = conf and conf.format
local pattern
if fmt then
pattern = fmt:gsub("%%.", "%%S+")
else
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
end,
}
---@param name string
---@return nil|oil.ColumnDefinition
M.get_column = function(name)
return file_columns[name]
end
M.supported_cross_adapter_actions = { files = "move" }
---@param action oil.Action
---@return boolean
M.filter_action = function(action)
if action.type == "create" then
return false
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
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))
return src_adapter.name == "files" or dest_adapter.name == "files"
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))
return src_adapter.name == "files" or dest_adapter.name == "files"
else
error(string.format("Bad action type '%s'", action.type))
end
end
---@param err oil.ParseError
---@return boolean
M.filter_error = function(err)
if err.message == "Duplicate filename" then
return false
end
return true
end
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == "delete" then
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 short_path = fs.shorten_path(trash_info.original_path)
return string.format(" PURGE %s", short_path)
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.name == "files" then
local _, path = util.parse_url(action.src_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" TRASH %s", short_path)
elseif dest_adapter.name == "files" then
local _, path = util.parse_url(action.dest_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path)
else
error("Must be moving files into or out of trash")
end
elseif action.type == "copy" then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then
local _, path = util.parse_url(action.src_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" COPY %s -> TRASH", short_path)
elseif dest_adapter.name == "files" then
local _, path = util.parse_url(action.dest_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path)
else
error("Must be copying files into or out of trash")
end
else
error(string.format("Bad action type '%s'", action.type))
end
end
---@param trash_info oil.TrashInfo
---@param cb fun(err?: string)
local function purge(trash_info, cb)
fs.recursive_delete("file", trash_info.info_file, function(err)
if err then
return cb(err)
end
---@diagnostic disable-next-line: undefined-field
fs.recursive_delete(trash_info.stat.type, trash_info.trash_file, cb)
end)
end
---@param path string
---@param info_path string
---@param cb fun(err?: string)
local function write_info_file(path, info_path, cb)
uv.fs_open(
info_path,
"w",
448,
vim.schedule_wrap(function(err, fd)
if err then
return cb(err)
end
assert(fd)
local deletion_date = vim.fn.strftime("%Y-%m-%dT%H:%M:%S")
local contents = string.format("[Trash Info]\nPath=%s\nDeletionDate=%s", path, deletion_date)
uv.fs_write(fd, contents, function(write_err)
uv.fs_close(fd, function(close_err)
cb(write_err or close_err)
end)
end)
end)
)
end
---@param path string
---@param cb fun(err?: string, trash_info?: oil.TrashInfo)
local function create_trash_info(path, cb)
local trash_dir = get_write_trash_dir(path)
local basename = vim.fs.basename(path)
local now = os.time()
local name = string.format("%s-%d.%d", basename, now, math.random(100000, 999999))
local dest_path = fs.join(trash_dir, "files", name)
local dest_info = fs.join(trash_dir, "info", name .. ".trashinfo")
uv.fs_lstat(path, function(err, stat)
if err then
return cb(err)
end
assert(stat)
write_info_file(path, dest_info, function(info_err)
if info_err then
return cb(info_err)
end
---@type oil.TrashInfo
local trash_info = {
original_path = path,
trash_file = dest_path,
info_file = dest_info,
deletion_date = now,
stat = stat,
}
cb(nil, trash_info)
end)
end)
end
---@param action oil.Action
---@param cb fun(err: nil|string)
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]
---@type oil.TrashInfo
local trash_info = assert(meta).trash_info
purge(trash_info, 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))
if src_adapter.name == "files" then
local _, path = util.parse_url(action.src_url)
M.delete_to_trash(assert(path), cb)
elseif dest_adapter.name == "files" then
-- Restore
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
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
fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err)
if err then
return cb(err)
end
uv.fs_unlink(trash_info.info_file, cb)
end)
else
error("Must be moving files into or out of trash")
end
elseif action.type == "copy" then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then
local _, path = util.parse_url(action.src_url)
assert(path)
create_trash_info(path, function(err, trash_info)
if err then
cb(err)
else
local stat_type = trash_info.stat.type or "unknown"
fs.recursive_copy(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
end
end)
elseif dest_adapter.name == "files" then
-- Restore
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
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
fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
else
error("Must be moving files into or out of trash")
end
else
cb(string.format("Bad action type: %s", action.type))
end
end
---@param path string
---@param cb fun(err?: string)
M.delete_to_trash = function(path, cb)
create_trash_info(path, function(err, trash_info)
if err then
cb(err)
else
local stat_type = trash_info.stat.type or "unknown"
fs.recursive_move(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
end
end)
end
return M

View file

@ -0,0 +1,232 @@
local cache = require("oil.cache")
local config = require("oil.config")
local files = require("oil.adapters.files")
local fs = require("oil.fs")
local util = require("oil.util")
local uv = vim.uv or vim.loop
local M = {}
local function touch_dir(path)
uv.fs_mkdir(path, 448) -- 0700
end
---Gets the location of the home trash dir, creating it if necessary
---@return string
local function get_trash_dir()
local trash_dir = fs.join(assert(uv.os_homedir()), ".Trash")
touch_dir(trash_dir)
return trash_dir
end
---@param url string
---@param callback fun(url: string)
M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url)
assert(path)
callback(scheme .. "/")
end
---@param url string
---@param entry oil.Entry
---@param cb fun(path: string)
M.get_entry_path = function(url, entry, cb)
local trash_dir = get_trash_dir()
local path = fs.join(trash_dir, entry.name)
if entry.type == "directory" then
path = "oil://" .. path
end
cb(path)
end
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, cb)
cb = vim.schedule_wrap(cb)
local _, path = util.parse_url(url)
assert(path)
local trash_dir = get_trash_dir()
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(trash_dir, function(open_err, fd)
if open_err then
if open_err:match("^ENOENT: no such file or directory") then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse
-- and edit a not-yet-existing directory.
return cb()
else
return cb(open_err)
end
end
local read_next
read_next = function()
uv.fs_readdir(fd, function(err, entries)
if err then
uv.fs_closedir(fd, function()
cb(err)
end)
return
elseif entries then
local internal_entries = {}
local poll = util.cb_collect(#entries, function(inner_err)
if inner_err then
cb(inner_err)
else
cb(nil, internal_entries, read_next)
end
end)
for _, entry in ipairs(entries) do
-- TODO: read .DS_Store and filter by original dir
local cache_entry = cache.create_entry(url, entry.name, entry.type)
table.insert(internal_entries, cache_entry)
poll()
end
else
uv.fs_closedir(fd, function(close_err)
if close_err then
cb(close_err)
else
cb()
end
end)
end
end)
end
read_next()
---@diagnostic disable-next-line: param-type-mismatch
end, 10000)
end
---@param bufnr integer
---@return boolean
M.is_modifiable = function(bufnr)
return true
end
---@param name string
---@return nil|oil.ColumnDefinition
M.get_column = function(name)
return nil
end
M.supported_cross_adapter_actions = { files = "move" }
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == "create" then
return string.format("CREATE %s", action.url)
elseif action.type == "delete" then
return string.format(" PURGE %s", action.url)
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.name == "files" then
local _, path = util.parse_url(action.src_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" TRASH %s", short_path)
elseif dest_adapter.name == "files" then
local _, path = util.parse_url(action.dest_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path)
else
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url)
end
elseif action.type == "copy" then
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url)
else
error("Bad action type")
end
end
---@param action oil.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
local trash_dir = get_trash_dir()
if action.type == "create" then
local _, path = util.parse_url(action.url)
assert(path)
path = trash_dir .. path
if action.entry_type == "directory" then
uv.fs_mkdir(path, 493, function(err)
-- Ignore if the directory already exists
if not err or err:match("^EEXIST:") then
cb()
else
cb(err)
end
end) -- 0755
elseif action.entry_type == "link" and action.link then
local flags = nil
local target = fs.posix_to_os_path(action.link)
---@diagnostic disable-next-line: param-type-mismatch
uv.fs_symlink(target, path, flags, cb)
else
fs.touch(path, cb)
end
elseif action.type == "delete" then
local _, path = util.parse_url(action.url)
assert(path)
local fullpath = trash_dir .. path
fs.recursive_delete(action.entry_type, fullpath, cb)
elseif action.type == "move" or 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_path = util.parse_url(action.src_url)
local _, dest_path = util.parse_url(action.dest_url)
assert(src_path and dest_path)
if src_adapter.name == "files" then
dest_path = trash_dir .. dest_path
elseif dest_adapter.name == "files" then
src_path = trash_dir .. src_path
else
dest_path = trash_dir .. dest_path
src_path = trash_dir .. src_path
end
if action.type == "move" then
fs.recursive_move(action.entry_type, src_path, dest_path, cb)
else
fs.recursive_copy(action.entry_type, src_path, dest_path, cb)
end
else
cb(string.format("Bad action type: %s", action.type))
end
end
---@param path string
---@param cb fun(err?: string)
M.delete_to_trash = function(path, cb)
local basename = vim.fs.basename(path)
local trash_dir = get_trash_dir()
local dest = fs.join(trash_dir, basename)
uv.fs_lstat(
path,
vim.schedule_wrap(function(stat_err, src_stat)
if stat_err then
return cb(stat_err)
end
assert(src_stat)
if uv.fs_lstat(dest) then
local date_str = vim.fn.strftime(" %Y-%m-%dT%H:%M:%S")
local name_pieces = vim.split(basename, ".", { plain = true })
if #name_pieces > 1 then
table.insert(name_pieces, #name_pieces - 1, date_str)
basename = table.concat(name_pieces)
else
basename = basename .. date_str
end
dest = fs.join(trash_dir, basename)
end
local stat_type = src_stat.type
fs.recursive_move(stat_type, path, dest, vim.schedule_wrap(cb))
end)
)
end
return M

View file

@ -0,0 +1,410 @@
local util = require("oil.util")
local uv = vim.uv or vim.loop
local cache = require("oil.cache")
local config = require("oil.config")
local constants = require("oil.constants")
local files = require("oil.adapters.files")
local fs = require("oil.fs")
local powershell_trash = require("oil.adapters.trash.windows.powershell-trash")
local FIELD_META = constants.FIELD_META
local FIELD_TYPE = constants.FIELD_TYPE
local M = {}
---@return string
local function get_trash_dir()
local cwd = assert(vim.fn.getcwd())
local trash_dir = cwd:sub(1, 3) .. "$Recycle.Bin"
if vim.fn.isdirectory(trash_dir) == 1 then
return trash_dir
end
trash_dir = "C:\\$Recycle.Bin"
if vim.fn.isdirectory(trash_dir) == 1 then
return trash_dir
end
error("No trash found")
end
---@param path string
---@return string
local win_addslash = function(path)
if not vim.endswith(path, "\\") then
return path .. "\\"
else
return path
end
end
---@class oil.WindowsTrashInfo
---@field trash_file string
---@field original_path string
---@field deletion_date integer
---@field info_file? string
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, cb)
local _, path = util.parse_url(url)
path = fs.posix_to_os_path(assert(path))
local trash_dir = get_trash_dir()
local show_all_files = fs.is_subpath(path, trash_dir)
powershell_trash.list_raw_entries(function(err, raw_entries)
if err then
cb(err)
return
end
local raw_displayed_entries = vim.tbl_filter(
---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string}
function(entry)
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h")))
local is_in_path = path == parent
local is_subpath = fs.is_subpath(path, parent)
return is_in_path or is_subpath or show_all_files
end,
raw_entries
)
local displayed_entries = vim.tbl_map(
---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string}
---@return {[1]:nil, [2]:string, [3]:string, [4]:{stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}}
function(entry)
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h")))
--- @type oil.InternalEntry
local cache_entry
if path == parent or show_all_files then
local deleted_file_tail = assert(vim.fn.fnamemodify(entry.Path, ":t"))
local deleted_file_head = assert(vim.fn.fnamemodify(entry.Path, ":h"))
local info_file_head = deleted_file_head
--- @type string?
local info_file
cache_entry =
cache.create_entry(url, deleted_file_tail, entry.IsFolder and "directory" or "file")
-- info_file on windows has the following format: $I<6 char hash>.<extension>
-- the hash is the same for the deleted file and the info file
-- so, we take the hash (and extension) from the deleted file
--
-- see https://superuser.com/questions/368890/how-does-the-recycle-bin-in-windows-work/1736690#1736690
local info_file_tail = deleted_file_tail:match("^%$R(.*)$") --[[@as string?]]
if info_file_tail then
info_file_tail = "$I" .. info_file_tail
info_file = info_file_head .. "\\" .. info_file_tail
end
cache_entry[FIELD_META] = {
stat = nil,
---@type oil.WindowsTrashInfo
trash_info = {
trash_file = entry.Path,
original_path = entry.OriginalPath,
deletion_date = entry.DeletionDate,
info_file = info_file,
},
display_name = entry.Name,
}
end
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name)
while next_par ~= "." do
name = next_par
next_par = vim.fs.dirname(name)
cache_entry = cache.create_entry(url, name, "directory")
cache_entry[FIELD_META] = {}
end
end
return cache_entry
end,
raw_displayed_entries
)
cb(nil, displayed_entries)
end)
end
M.is_modifiable = function(_bufnr)
return true
end
local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function()
current_year = vim.fn.strftime("%Y")
end)
local file_columns = {}
file_columns.mtime = {
render = function(entry, conf)
local meta = entry[FIELD_META]
if not meta then
return nil
end
---@type oil.WindowsTrashInfo
local trash_info = meta.trash_info
local time = trash_info and trash_info.deletion_date
if not time then
return nil
end
local fmt = conf and conf.format
local ret
if fmt then
ret = vim.fn.strftime(fmt, time)
else
local year = vim.fn.strftime("%Y", time)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", time)
else
ret = vim.fn.strftime("%b %d %H:%M", time)
end
end
return ret
end,
get_sort_value = function(entry)
local meta = entry[FIELD_META]
---@type nil|oil.WindowsTrashInfo
local trash_info = meta and meta.trash_info
if trash_info and trash_info.deletion_date then
return trash_info.deletion_date
else
return 0
end
end,
parse = function(line, conf)
local fmt = conf and conf.format
local pattern
if fmt then
pattern = fmt:gsub("%%.", "%%S+")
else
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
end,
}
---@param name string
---@return nil|oil.ColumnDefinition
M.get_column = function(name)
return file_columns[name]
end
---@param action oil.Action
---@return boolean
M.filter_action = function(action)
if action.type == "create" then
return false
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
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))
return src_adapter.name == "files" or dest_adapter.name == "files"
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))
return src_adapter.name == "files" or dest_adapter.name == "files"
else
error(string.format("Bad action type '%s'", action.type))
end
end
---@param url string
---@param callback fun(url: string)
M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url)
assert(path)
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p")
assert(os_path)
uv.fs_realpath(
os_path,
vim.schedule_wrap(function(_err, new_os_path)
local realpath = new_os_path or os_path
callback(scheme .. util.addslash(fs.os_to_posix_path(realpath)))
end)
)
end
---@param url string
---@param entry oil.Entry
---@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 = internal_entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
local trash_info = meta and meta.trash_info
if not trash_info then
-- This is a subpath in the trash
M.normalize_url(url, cb)
return
end
local path = fs.os_to_posix_path(trash_info.trash_file)
if entry.type == "directory" then
path = win_addslash(path)
end
cb("oil://" .. path)
end
---@param err oil.ParseError
---@return boolean
M.filter_error = function(err)
if err.message == "Duplicate filename" then
return false
end
return true
end
---@param action oil.Action
---@return string
M.render_action = function(action)
if action.type == "delete" then
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 short_path = fs.shorten_path(trash_info.original_path)
return string.format(" PURGE %s", short_path)
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.name == "files" then
local _, path = util.parse_url(action.src_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" TRASH %s", short_path)
elseif dest_adapter.name == "files" then
local _, path = util.parse_url(action.dest_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path)
else
error("Must be moving files into or out of trash")
end
elseif action.type == "copy" then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then
local _, path = util.parse_url(action.src_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" COPY %s -> TRASH", short_path)
elseif dest_adapter.name == "files" then
local _, path = util.parse_url(action.dest_url)
assert(path)
local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path)
else
error("Must be copying files into or out of trash")
end
else
error(string.format("Bad action type '%s'", action.type))
end
end
---@param trash_info oil.WindowsTrashInfo
---@param cb fun(err?: string, raw_entries: oil.WindowsRawEntry[]?)
local purge = function(trash_info, cb)
fs.recursive_delete("file", trash_info.info_file, function(err)
if err then
return cb(err)
end
fs.recursive_delete("file", trash_info.trash_file, cb)
end)
end
---@param path string
---@param type string
---@param cb fun(err?: string, trash_info?: oil.TrashInfo)
local function create_trash_info_and_copy(path, type, cb)
local temp_path = path .. "temp"
-- create a temporary copy on the same location
fs.recursive_copy(
type,
path,
temp_path,
vim.schedule_wrap(function(err)
if err then
return cb(err)
end
-- delete original file
M.delete_to_trash(path, function(err2)
if err2 then
return cb(err2)
end
-- rename temporary copy to the original file name
fs.recursive_move(type, temp_path, path, cb)
end)
end)
)
end
---@param action oil.Action
---@param cb fun(err: nil|string)
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
purge(trash_info, 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))
if src_adapter.name == "files" then
local _, path = util.parse_url(action.src_url)
M.delete_to_trash(assert(path), cb)
elseif dest_adapter.name == "files" then
-- Restore
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
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
fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err)
if err then
return cb(err)
end
uv.fs_unlink(trash_info.info_file, cb)
end)
end
elseif action.type == "copy" then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then
local _, path = util.parse_url(action.src_url)
assert(path)
path = fs.posix_to_os_path(path)
local entry = assert(cache.get_entry_by_url(action.src_url))
create_trash_info_and_copy(path, entry[FIELD_TYPE], cb)
elseif dest_adapter.name == "files" then
-- Restore
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
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
fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
else
error("Must be moving files into or out of trash")
end
else
cb(string.format("Bad action type: %s", action.type))
end
end
M.supported_cross_adapter_actions = { files = "move" }
---@param path string
---@param cb fun(err?: string)
M.delete_to_trash = function(path, cb)
powershell_trash.delete_to_trash(path, cb)
end
return M

View file

@ -0,0 +1,123 @@
---@class (exact) oil.PowershellCommand
---@field cmd string
---@field cb fun(err?: string, output?: string)
---@field running? boolean
---@class oil.PowershellConnection
---@field private jid integer
---@field private execution_error? string
---@field private commands oil.PowershellCommand[]
---@field private stdout string[]
---@field private is_reading_data boolean
local PowershellConnection = {}
---@param init_command? string
---@return oil.PowershellConnection
function PowershellConnection.new(init_command)
local self = setmetatable({
commands = {},
stdout = {},
is_reading_data = false,
}, { __index = PowershellConnection })
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({
"cmd",
"/c",
'"chcp 65001 && powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -NoExit -Command -"',
}, {
---@param data string[]
on_stdout = function(_, data)
for _, fragment in ipairs(data) do
if fragment:find("===DONE%((%a+)%)===") then
self.is_reading_data = false
local output = table.concat(self.stdout, "")
local cb = self.commands[1].cb
table.remove(self.commands, 1)
local success = fragment:match("===DONE%((%a+)%)===")
if success == "True" then
cb(nil, output)
elseif success == "False" then
cb(success .. ": " .. output, output)
end
self.stdout = {}
self:_consume()
elseif self.is_reading_data then
table.insert(self.stdout, fragment)
end
end
end,
})
vim.o.shellslash = saved_shellslash
if jid == 0 then
self:_set_error("passed invalid arguments to 'powershell'")
elseif jid == -1 then
self:_set_error("'powershell' is not executable")
else
self.jid = jid
end
if init_command then
table.insert(self.commands, { cmd = init_command, cb = function() end })
self:_consume()
end
end
---@param command string
---@param cb fun(err?: string, output?: string[])
function PowershellConnection:run(command, cb)
if self.execution_error then
cb(self.execution_error)
else
table.insert(self.commands, { cmd = command, cb = cb })
self:_consume()
end
end
function PowershellConnection:_consume()
if not vim.tbl_isempty(self.commands) then
local cmd = self.commands[1]
if not cmd.running then
cmd.running = true
self.is_reading_data = true
-- $? contains the execution status of the last command.
-- see https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.4#section-1
vim.api.nvim_chan_send(self.jid, cmd.cmd .. '\nWrite-Host "===DONE($?)==="\n')
end
end
end
---@param err string
function PowershellConnection:_set_error(err)
if self.execution_error then
return
end
self.execution_error = err
local commands = self.commands
self.commands = {}
for _, cmd in ipairs(commands) do
cmd.cb(err)
end
end
return PowershellConnection

View file

@ -0,0 +1,78 @@
-- A wrapper around trash operations using windows powershell
local Powershell = require("oil.adapters.trash.windows.powershell-connection")
---@class oil.WindowsRawEntry
---@field IsFolder boolean
---@field DeletionDate integer
---@field Name string
---@field Path string
---@field OriginalPath string
local M = {}
-- 0xa is the constant for Recycle Bin. See https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants
local list_entries_init = [[
$shell = New-Object -ComObject 'Shell.Application'
$folder = $shell.NameSpace(0xa)
]]
local list_entries_cmd = [[
$data = @(foreach ($i in $folder.items())
{
@{
IsFolder=$i.IsFolder;
DeletionDate=([DateTimeOffset]$i.extendedproperty('datedeleted')).ToUnixTimeSeconds();
Name=$i.Name;
Path=$i.Path;
OriginalPath=-join($i.ExtendedProperty('DeletedFrom'), "\", $i.Name)
}
})
ConvertTo-Json $data -Compress
]]
---@type nil|oil.PowershellConnection
local list_entries_powershell
---@param cb fun(err?: string, raw_entries?: oil.WindowsRawEntry[])
M.list_raw_entries = function(cb)
if not list_entries_powershell then
list_entries_powershell = Powershell.new(list_entries_init)
end
list_entries_powershell:run(list_entries_cmd, function(err, string)
if err then
cb(err)
return
end
local ok, value = pcall(vim.json.decode, string)
if not ok then
cb(value)
return
end
cb(nil, value)
end)
end
-- 0 is the constant for Windows Desktop. See https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants
local delete_init = [[
$shell = New-Object -ComObject 'Shell.Application'
$folder = $shell.NameSpace(0)
]]
local delete_cmd = [[
$path = Get-Item '%s'
$folder.ParseName($path.FullName).InvokeVerb('delete')
]]
---@type nil|oil.PowershellConnection
local delete_to_trash_powershell
---@param path string
---@param cb fun(err?: string)
M.delete_to_trash = function(path, cb)
if not delete_to_trash_powershell then
delete_to_trash_powershell = Powershell.new(delete_init)
end
delete_to_trash_powershell:run((delete_cmd):format(path:gsub("'", "''")), cb)
end
return M

View file

@ -4,10 +4,12 @@ local M = {}
local FIELD_ID = constants.FIELD_ID local FIELD_ID = constants.FIELD_ID
local FIELD_NAME = constants.FIELD_NAME local FIELD_NAME = constants.FIELD_NAME
local FIELD_META = constants.FIELD_META
local next_id = 1 local next_id = 1
-- Map<url, Map<entry name, oil.InternalEntry>> -- Map<url, Map<entry name, oil.InternalEntry>>
---@type table<string, table<string, oil.InternalEntry>>
local url_directory = {} local url_directory = {}
---@type table<integer, oil.InternalEntry> ---@type table<integer, oil.InternalEntry>
@ -118,6 +120,16 @@ M.get_entry_by_id = function(id)
return entries_by_id[id] return entries_by_id[id]
end end
---@param url string
---@return nil|oil.InternalEntry
M.get_entry_by_url = function(url)
local scheme, path = util.parse_url(url)
assert(path)
local parent_url = scheme .. vim.fn.fnamemodify(path, ":h")
local basename = vim.fn.fnamemodify(path, ":t")
return M.list_url(parent_url)[basename]
end
---@param id integer ---@param id integer
---@return string ---@return string
M.get_parent_url = function(id) M.get_parent_url = function(id)
@ -129,27 +141,23 @@ M.get_parent_url = function(id)
end end
---@param url string ---@param url string
---@return oil.InternalEntry[] ---@return table<string, oil.InternalEntry>
M.list_url = function(url) M.list_url = function(url)
url = util.addslash(url) url = util.addslash(url)
return url_directory[url] or {} return url_directory[url] or {}
end end
M.get_entry_by_url = function(url)
local parent, name = url:match("^(.+)/([^/]+)$")
local cache = url_directory[parent]
return cache and cache[name]
end
---@param action oil.Action ---@param action oil.Action
M.perform_action = function(action) M.perform_action = function(action)
if action.type == "create" then if action.type == "create" then
local scheme, path = util.parse_url(action.url) local scheme, path = util.parse_url(action.url)
assert(path)
local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h"))
local name = vim.fn.fnamemodify(path, ":t") local name = vim.fn.fnamemodify(path, ":t")
M.create_and_store_entry(parent_url, name, action.entry_type) M.create_and_store_entry(parent_url, name, action.entry_type)
elseif action.type == "delete" then elseif action.type == "delete" then
local scheme, path = util.parse_url(action.url) local scheme, path = util.parse_url(action.url)
assert(path)
local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h"))
local name = vim.fn.fnamemodify(path, ":t") local name = vim.fn.fnamemodify(path, ":t")
local entry = url_directory[parent_url][name] local entry = url_directory[parent_url][name]
@ -158,11 +166,13 @@ M.perform_action = function(action)
parent_url_by_id[entry[FIELD_ID]] = nil parent_url_by_id[entry[FIELD_ID]] = nil
elseif action.type == "move" then elseif action.type == "move" then
local src_scheme, src_path = util.parse_url(action.src_url) local src_scheme, src_path = util.parse_url(action.src_url)
assert(src_path)
local src_parent_url = util.addslash(src_scheme .. vim.fn.fnamemodify(src_path, ":h")) local src_parent_url = util.addslash(src_scheme .. vim.fn.fnamemodify(src_path, ":h"))
local src_name = vim.fn.fnamemodify(src_path, ":t") local src_name = vim.fn.fnamemodify(src_path, ":t")
local entry = url_directory[src_parent_url][src_name] local entry = url_directory[src_parent_url][src_name]
local dest_scheme, dest_path = util.parse_url(action.dest_url) local dest_scheme, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
local dest_parent_url = util.addslash(dest_scheme .. vim.fn.fnamemodify(dest_path, ":h")) local dest_parent_url = util.addslash(dest_scheme .. vim.fn.fnamemodify(dest_path, ":h"))
local dest_name = vim.fn.fnamemodify(dest_path, ":t") local dest_name = vim.fn.fnamemodify(dest_path, ":t")
@ -172,18 +182,22 @@ M.perform_action = function(action)
dest_parent = {} dest_parent = {}
url_directory[dest_parent_url] = dest_parent url_directory[dest_parent_url] = dest_parent
end end
-- We have to clear the metadata because it can be inaccurate after the move
entry[FIELD_META] = nil
dest_parent[dest_name] = entry dest_parent[dest_name] = entry
parent_url_by_id[entry[FIELD_ID]] = dest_parent_url parent_url_by_id[entry[FIELD_ID]] = dest_parent_url
entry[FIELD_NAME] = dest_name entry[FIELD_NAME] = dest_name
util.update_moved_buffers(action.entry_type, action.src_url, action.dest_url) util.update_moved_buffers(action.entry_type, action.src_url, action.dest_url)
elseif action.type == "copy" then elseif action.type == "copy" then
local scheme, path = util.parse_url(action.dest_url) local scheme, path = util.parse_url(action.dest_url)
assert(path)
local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h"))
local name = vim.fn.fnamemodify(path, ":t") local name = vim.fn.fnamemodify(path, ":t")
M.create_and_store_entry(parent_url, name, action.entry_type) M.create_and_store_entry(parent_url, name, action.entry_type)
elseif action.type == "change" then elseif action.type == "change" then
-- Cache doesn't need to update -- Cache doesn't need to update
else else
---@diagnostic disable-next-line: undefined-field
error(string.format("Bad action type: '%s'", action.type)) error(string.format("Bad action type: '%s'", action.type))
end end
end end

370
lua/oil/clipboard.lua Normal file
View file

@ -0,0 +1,370 @@
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("<Esc>", 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

View file

@ -1,7 +1,6 @@
local config = require("oil.config") local config = require("oil.config")
local constants = require("oil.constants") local constants = require("oil.constants")
local util = require("oil.util") local util = require("oil.util")
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
local M = {} local M = {}
local FIELD_NAME = constants.FIELD_NAME local FIELD_NAME = constants.FIELD_NAME
@ -10,16 +9,16 @@ local FIELD_META = constants.FIELD_META
local all_columns = {} local all_columns = {}
---@alias oil.ColumnSpec string|table ---@alias oil.ColumnSpec string|{[1]: string, [string]: any}
---@class (exact) oil.ColumnDefinition ---@class (exact) oil.ColumnDefinition
---@field render fun(entry: oil.InternalEntry, conf: nil|table): nil|oil.TextChunk ---@field render fun(entry: oil.InternalEntry, conf: nil|table, bufnr: integer): nil|oil.TextChunk
---@field parse fun(line: string, conf: nil|table): nil|string, nil|string ---@field parse fun(line: string, conf: nil|table): nil|string, nil|string
---@field meta_fields? table<string, fun(parent_url: string, entry: oil.InternalEntry, cb: fun(err: nil|string))>
---@field compare? fun(entry: oil.InternalEntry, parsed_value: any): boolean ---@field compare? fun(entry: oil.InternalEntry, parsed_value: any): boolean
---@field render_action? fun(action: oil.ChangeAction): string ---@field render_action? fun(action: oil.ChangeAction): string
---@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string)) ---@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string))
---@field get_sort_value? fun(entry: oil.InternalEntry): number|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 name string
---@param column oil.ColumnDefinition ---@param column oil.ColumnDefinition
@ -54,55 +53,16 @@ M.get_supported_columns = function(adapter_or_scheme)
return ret return ret
end end
---@param adapter oil.Adapter local EMPTY = { "-", "OilEmpty" }
---@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 M.EMPTY = EMPTY
---@param adapter oil.Adapter ---@param adapter oil.Adapter
---@param col_def oil.ColumnSpec ---@param col_def oil.ColumnSpec
---@param entry oil.InternalEntry ---@param entry oil.InternalEntry
---@param bufnr integer
---@return oil.TextChunk ---@return oil.TextChunk
M.render_col = function(adapter, col_def, entry) M.render_col = function(adapter, col_def, entry, bufnr)
local name, conf = util.split_config(col_def) local name, conf = util.split_config(col_def)
local column = M.get_column(adapter, name) local column = M.get_column(adapter, name)
if not column then if not column then
@ -110,19 +70,7 @@ M.render_col = function(adapter, col_def, entry)
return EMPTY return EMPTY
end end
-- Make sure all the required metadata exists before attempting to render local chunk = column.render(entry, conf, bufnr)
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 type(chunk) == "table" then
if chunk[1]:match("^%s*$") then if chunk[1]:match("^%s*$") then
return EMPTY return EMPTY
@ -150,12 +98,13 @@ end
M.parse_col = function(adapter, line, col_def) M.parse_col = function(adapter, line, col_def)
local name, conf = util.split_config(col_def) local name, conf = util.split_config(col_def)
-- If rendering failed, there will just be a "-" -- If rendering failed, there will just be a "-"
if vim.startswith(line, "- ") then local empty_col, rem = line:match("^%s*(-%s+)(.*)$")
return nil, line:sub(3) if empty_col then
return nil, rem
end end
local column = M.get_column(adapter, name) local column = M.get_column(adapter, name)
if column then if column then
return column.parse(line, conf) return column.parse(line:gsub("^%s+", ""), conf)
end end
end end
@ -201,31 +150,35 @@ M.perform_change_action = function(adapter, action, callback)
column.perform_action(action, callback) column.perform_action(action, callback)
end end
if has_devicons then local icon_provider = util.get_icon_provider()
if icon_provider then
M.register("icon", { M.register("icon", {
render = function(entry, conf) render = function(entry, conf)
local type = entry[FIELD_TYPE] local field_type = entry[FIELD_TYPE]
local name = entry[FIELD_NAME] local name = entry[FIELD_NAME]
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if type == "link" and meta then if field_type == "link" and meta then
if meta.link then if meta.link then
name = meta.link name = meta.link
end end
if meta.link_stat then if meta.link_stat then
type = meta.link_stat.type field_type = meta.link_stat.type
end end
end end
local icon, hl if meta and meta.display_name then
if type == "directory" then name = meta.display_name
icon = conf and conf.directory or ""
hl = "OilDirIcon"
else
icon, hl = devicons.get_icon(name)
icon = icon or (conf and conf.default_file or "")
end end
local icon, hl = icon_provider(field_type, name, conf)
if not conf or conf.add_padding ~= false then if not conf or conf.add_padding ~= false then
icon = icon .. " " icon = icon .. " "
end end
if conf and conf.highlight then
if type(conf.highlight) == "function" then
hl = conf.highlight(icon)
else
hl = conf.highlight
end
end
return { icon, hl } return { icon, hl }
end, end,
@ -247,7 +200,7 @@ local function is_entry_directory(entry)
return true return true
elseif type == "link" then elseif type == "link" then
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
return meta and meta.link_stat and meta.link_stat.type == "directory" return (meta and meta.link_stat and meta.link_stat.type == "directory") == true
else else
return false return false
end end
@ -275,6 +228,10 @@ M.register("type", {
end, end,
}) })
local function adjust_number(int)
return string.format("%03d%s", #int, int)
end
M.register("name", { M.register("name", {
render = function(entry, conf) render = function(entry, conf)
error("Do not use the name column. It is for sorting only") error("Do not use the name column. It is for sorting only")
@ -284,8 +241,33 @@ M.register("name", {
error("Do not use the name column. It is for sorting only") error("Do not use the name column. It is for sorting only")
end, end,
get_sort_value = function(entry) create_sort_value_factory = function(num_entries)
return entry[FIELD_NAME] 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
end
end, end,
}) })

View file

@ -1,6 +1,6 @@
local default_config = { local default_config = {
-- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`)
-- Set to false if you still want to use netrw. -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories.
default_file_explorer = true, default_file_explorer = true,
-- Id is automatically added at the beginning, and name at the end -- Id is automatically added at the beginning, and name at the end
-- See :help oil-columns -- See :help oil-columns
@ -28,16 +28,29 @@ local default_config = {
}, },
-- Send deleted files to the trash instead of permanently deleting them (:help oil-trash) -- Send deleted files to the trash instead of permanently deleting them (:help oil-trash)
delete_to_trash = false, delete_to_trash = false,
-- Skip the confirmation popup for simple operations -- Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits)
skip_confirm_for_simple_edits = false, skip_confirm_for_simple_edits = false,
-- Change this to customize the command used when deleting to trash
trash_command = "trash-put",
-- Selecting a new/moved/renamed file or directory will prompt you to save changes first -- Selecting a new/moved/renamed file or directory will prompt you to save changes first
-- (:help prompt_save_on_select_new_entry)
prompt_save_on_select_new_entry = true, prompt_save_on_select_new_entry = true,
-- Oil will automatically delete hidden buffers after this delay -- Oil will automatically delete hidden buffers after this delay
-- You can set the delay to false to disable cleanup entirely -- You can set the delay to false to disable cleanup entirely
-- Note that the cleanup process only starts when none of the oil buffers are currently displayed -- Note that the cleanup process only starts when none of the oil buffers are currently displayed
cleanup_delay_ms = 2000, 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
-- Set to "unmodified" to only save unmodified buffers
autosave_changes = false,
},
-- Constrain the cursor to the editable parts of the oil buffer
-- Set to `false` to disable, or "name" to keep it on the file names
constrain_cursor = "editable",
-- Set to true to watch the filesystem for changes and reload oil
watch_for_changes = false,
-- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap
-- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" })
-- Additionally, if it is a string that matches "actions.<name>", -- Additionally, if it is a string that matches "actions.<name>",
@ -45,21 +58,22 @@ local default_config = {
-- Set to `false` to remove a keymap -- Set to `false` to remove a keymap
-- See :help oil-actions for a list of all available actions -- See :help oil-actions for a list of all available actions
keymaps = { keymaps = {
["g?"] = "actions.show_help", ["g?"] = { "actions.show_help", mode = "n" },
["<CR>"] = "actions.select", ["<CR>"] = "actions.select",
["<C-s>"] = "actions.select_vsplit", ["<C-s>"] = { "actions.select", opts = { vertical = true } },
["<C-h>"] = "actions.select_split", ["<C-h>"] = { "actions.select", opts = { horizontal = true } },
["<C-t>"] = "actions.select_tab", ["<C-t>"] = { "actions.select", opts = { tab = true } },
["<C-p>"] = "actions.preview", ["<C-p>"] = "actions.preview",
["<C-c>"] = "actions.close", ["<C-c>"] = { "actions.close", mode = "n" },
["<C-l>"] = "actions.refresh", ["<C-l>"] = "actions.refresh",
["-"] = "actions.parent", ["-"] = { "actions.parent", mode = "n" },
["_"] = "actions.open_cwd", ["_"] = { "actions.open_cwd", mode = "n" },
["`"] = "actions.cd", ["`"] = { "actions.cd", mode = "n" },
["~"] = "actions.tcd", ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = "actions.change_sort", ["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external", ["gx"] = "actions.open_external",
["g."] = "actions.toggle_hidden", ["g."] = { "actions.toggle_hidden", mode = "n" },
["g\\"] = { "actions.toggle_trash", mode = "n" },
}, },
-- Set to false to disable all of the above keymaps -- Set to false to disable all of the above keymaps
use_default_keymaps = true, use_default_keymaps = true,
@ -68,37 +82,82 @@ local default_config = {
show_hidden = false, show_hidden = false,
-- This function defines what is considered a "hidden" file -- This function defines what is considered a "hidden" file
is_hidden_file = function(name, bufnr) is_hidden_file = function(name, bufnr)
return vim.startswith(name, ".") local m = name:match("^%.")
return m ~= nil
end, end,
-- This function defines what will never be shown, even when `show_hidden` is set -- This function defines what will never be shown, even when `show_hidden` is set
is_always_hidden = function(name, bufnr) is_always_hidden = function(name, bufnr)
return false return false
end, 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 and directory names case insensitive
case_insensitive = false,
sort = { sort = {
-- sort order can be "asc" or "desc" -- sort order can be "asc" or "desc"
-- see :help oil-columns to see which columns are sortable -- see :help oil-columns to see which columns are sortable
{ "type", "asc" }, { "type", "asc" },
{ "name", "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
add = function(path)
return false
end,
mv = function(src_path, dest_path)
return false
end,
rm = function(path)
return false
end,
}, },
-- Configuration for the floating window in oil.open_float -- Configuration for the floating window in oil.open_float
float = { float = {
-- Padding around the floating window -- Padding around the floating window
padding = 2, 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_width = 0,
max_height = 0, max_height = 0,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, 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. -- This is the config that will be passed to nvim_open_win.
-- Change values here to customize the layout -- Change values here to customize the layout
override = function(conf) override = function(conf)
return conf return conf
end, end,
}, },
-- Configuration for the actions floating preview window -- Configuration for the file preview window
preview = { 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 = {
-- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- 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. -- 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" -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
@ -115,7 +174,7 @@ local default_config = {
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window -- optionally define an integer/float for the exact height of the preview window
height = nil, height = nil,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -128,43 +187,264 @@ local default_config = {
max_height = { 10, 0.9 }, max_height = { 10, 0.9 },
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
height = nil, height = nil,
border = "rounded", border = nil,
minimized_border = "none", minimized_border = "none",
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
}, },
-- Configuration for the floating SSH window
ssh = {
border = nil,
},
-- Configuration for the floating keymaps help window
keymaps_help = {
border = nil,
},
} }
-- The adapter API hasn't really stabilized yet. We're not ready to advertise or encourage people to -- 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 -- 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. -- 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 = { default_config.adapters = {
["oil://"] = "files", ["oil://"] = "files",
["oil-ssh://"] = "ssh", ["oil-ssh://"] = "ssh",
[oil_s3_string] = "s3",
["oil-trash://"] = "trash",
} }
default_config.adapter_aliases = {} 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<string, string> Hidden from SetupOpts
---@field adapter_aliases table<string, string> Hidden from SetupOpts
---@field silence_scp_warning? boolean Undocumented option
---@field default_file_explorer boolean
---@field columns oil.ColumnSpec[]
---@field buf_options table<string, any>
---@field win_options table<string, any>
---@field delete_to_trash boolean
---@field skip_confirm_for_simple_edits boolean
---@field prompt_save_on_select_new_entry boolean
---@field cleanup_delay_ms integer
---@field lsp_file_methods oil.LspFileMethods
---@field constrain_cursor false|"name"|"editable"
---@field watch_for_changes boolean
---@field keymaps table<string, any>
---@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 progress oil.ProgressWindowConfig
---@field ssh oil.SimpleWindowConfig
---@field keymaps_help oil.SimpleWindowConfig
local M = {} local M = {}
-- For backwards compatibility
---@alias oil.setupOpts oil.SetupOpts
---@class (exact) oil.SetupOpts
---@field default_file_explorer? boolean Oil will take over directory buffers (e.g. `vim .` or `:e src/`). Set to false if you still want to use netrw.
---@field columns? oil.ColumnSpec[] The columns to display. See :help oil-columns.
---@field buf_options? table<string, any> Buffer-local options to use for oil buffers
---@field win_options? table<string, any> Window-local options to use for oil buffers
---@field delete_to_trash? boolean Send deleted files to the trash instead of permanently deleting them (:help oil-trash).
---@field skip_confirm_for_simple_edits? boolean Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits).
---@field prompt_save_on_select_new_entry? boolean Selecting a new/moved/renamed file or directory will prompt you to save changes first (:help prompt_save_on_select_new_entry).
---@field cleanup_delay_ms? integer Oil will automatically delete hidden buffers after this delay. You can set the delay to false to disable cleanup entirely. Note that the cleanup process only starts when none of the oil buffers are currently displayed.
---@field lsp_file_methods? oil.SetupLspFileMethods Configure LSP file operation integration.
---@field constrain_cursor? false|"name"|"editable" Constrain the cursor to the editable parts of the oil buffer. Set to `false` to disable, or "name" to keep it on the file names.
---@field watch_for_changes? boolean Set to true to watch the filesystem for changes and reload oil.
---@field keymaps? table<string, any>
---@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 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.
---@class (exact) oil.ViewOptions
---@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 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 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
---@field [2] "asc"|"desc"
---@class (exact) oil.GitOptions
---@field add fun(path: string): boolean
---@field mv fun(src_path: string, dest_path: string): boolean
---@field rm fun(path: string): boolean
---@class (exact) oil.SetupGitOptions
---@field add? fun(path: string): boolean Return true to automatically git add a new file
---@field mv? fun(src_path: string, dest_path: string): boolean Return true to automatically git mv a moved file
---@field rm? fun(path: string): boolean Return true to automatically git rm a deleted file
---@class (exact) oil.WindowDimensionDualConstraint
---@field [1] number
---@field [2] number
---@alias oil.WindowDimension number|oil.WindowDimensionDualConstraint
---@class (exact) oil.WindowConfig
---@field max_width oil.WindowDimension
---@field min_width oil.WindowDimension
---@field width? number
---@field max_height oil.WindowDimension
---@field min_height oil.WindowDimension
---@field height? number
---@field border string|string[]
---@field win_options table<string, any>
---@class (exact) oil.SetupWindowConfig
---@field max_width? oil.WindowDimension Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). 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"
---@field min_width? oil.WindowDimension Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. min_width = {40, 0.4} means "the greater of 40 columns or 40% of total"
---@field width? number Define an integer/float for the exact width of the preview window
---@field max_height? oil.WindowDimension Height dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. max_height = {80, 0.9} means "the lesser of 80 columns or 90% of total"
---@field min_height? oil.WindowDimension Height dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. min_height = {5, 0.1} means "the greater of 5 columns or 10% of total"
---@field height? number Define an integer/float for the exact height of the preview window
---@field border? string|string[] Window border
---@field win_options? table<string, any>
---@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
---@field update_on_cursor_moved boolean
---@field preview_method oil.PreviewMethod
---@field disable_preview fun(filename: string): boolean
---@field win_options table<string, any>
---@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig
---@class (exact) oil.SetupPreviewWindowConfig
---@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<string, any> 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[]
---@class (exact) oil.SetupProgressWindowConfig : oil.SetupWindowConfig
---@field minimized_border? string|string[] The border for the minimized progress window
---@class (exact) oil.FloatWindowConfig
---@field padding integer
---@field max_width integer
---@field max_height integer
---@field border string|string[]
---@field win_options table<string, any>
---@field get_win_title fun(winid: integer): string
---@field preview_split "auto"|"left"|"right"|"above"|"below"
---@field override fun(conf: table): table
---@class (exact) oil.SetupFloatWindowConfig
---@field padding? integer
---@field max_width? integer
---@field max_height? integer
---@field border? string|string[] Window border
---@field win_options? table<string, any>
---@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
---@class (exact) oil.SimpleWindowConfig
---@field border string|string[]
---@class (exact) oil.SetupSimpleWindowConfig
---@field border? string|string[] Window border
M.setup = function(opts) M.setup = function(opts)
local new_conf = vim.tbl_deep_extend("keep", opts or {}, default_config) opts = opts or {}
local new_conf = vim.tbl_deep_extend("keep", opts, default_config)
if not new_conf.use_default_keymaps then if not new_conf.use_default_keymaps then
new_conf.keymaps = opts.keymaps or {} 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 end
if new_conf.delete_to_trash then -- Backwards compatibility for old versions that don't support winborder
local trash_bin = vim.split(new_conf.trash_command, " ")[1] if vim.fn.has("nvim-0.11") == 0 then
if vim.fn.executable(trash_bin) == 0 then new_conf = vim.tbl_deep_extend("keep", new_conf, {
vim.notify( float = { border = "rounded" },
string.format( confirmation = { border = "rounded" },
"oil.nvim: delete_to_trash is true, but '%s' executable not found.\nDeleted files will be permanently removed.", progress = { border = "rounded" },
new_conf.trash_command ssh = { border = "rounded" },
), keymaps_help = { border = "rounded" },
vim.log.levels.WARN })
) end
new_conf.delete_to_trash = false
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
new_conf.lsp_file_methods.autosave_changes = new_conf.lsp_rename_autosave
new_conf.lsp_rename_autosave = nil
vim.notify_once(
"oil config value lsp_rename_autosave has moved to lsp_file_methods.autosave_changes.\nCompatibility will be removed on 2024-09-01.",
vim.log.levels.WARN
)
end
-- This option was renamed because it is no longer experimental
if new_conf.experimental_watch_for_changes then
new_conf.watch_for_changes = true
end end
for k, v in pairs(new_conf) do for k, v in pairs(new_conf) do
@ -176,46 +456,6 @@ M.setup = function(opts)
M.adapter_to_scheme[v] = k M.adapter_to_scheme[v] = k
end end
M._adapter_by_scheme = {} M._adapter_by_scheme = {}
if type(M.trash) == "string" then
M.trash = vim.fn.fnamemodify(vim.fn.expand(M.trash), ":p")
end
end
---@return nil|string
M.get_trash_url = function()
if not M.trash then
return nil
end
local fs = require("oil.fs")
if M.trash == true then
local data_home = os.getenv("XDG_DATA_HOME") or vim.fn.expand("~/.local/share")
local preferred = fs.join(data_home, "trash")
local candidates = {
preferred,
}
if fs.is_windows then
-- TODO permission issues when using the recycle bin. The folder gets created without
-- read/write perms, so all operations fail
-- local cwd = vim.fn.getcwd()
-- table.insert(candidates, 1, cwd:sub(1, 3) .. "$Recycle.Bin")
-- table.insert(candidates, 1, "C:\\$Recycle.Bin")
else
table.insert(candidates, fs.join(data_home, "Trash", "files"))
table.insert(candidates, fs.join(os.getenv("HOME"), ".Trash"))
end
local trash_dir = preferred
for _, candidate in ipairs(candidates) do
if vim.fn.isdirectory(candidate) == 1 then
trash_dir = candidate
break
end
end
local oil_trash_dir = vim.fn.fnamemodify(fs.join(trash_dir, "nvim", "oil"), ":p")
fs.mkdirp(oil_trash_dir)
M.trash = oil_trash_dir
end
return M.adapter_to_scheme.files .. fs.os_to_posix_path(M.trash)
end end
---@param scheme nil|string ---@param scheme nil|string
@ -236,10 +476,6 @@ M.get_adapter_by_scheme = function(scheme)
if adapter == nil then if adapter == nil then
local name = M.adapters[scheme] local name = M.adapters[scheme]
if not name then if not name then
vim.notify(
string.format("Could not find oil adapter for scheme '%s'", scheme),
vim.log.levels.ERROR
)
return nil return nil
end end
local ok local ok
@ -250,7 +486,6 @@ M.get_adapter_by_scheme = function(scheme)
else else
M._adapter_by_scheme[scheme] = false M._adapter_by_scheme[scheme] = false
adapter = false adapter = false
vim.notify(string.format("Could not find oil adapter '%s'", name), vim.log.levels.ERROR)
end end
end end
if adapter then if adapter then

View file

@ -2,7 +2,7 @@ local M = {}
---Store entries as a list-like table for maximum space efficiency and retrieval speed. ---Store entries as a list-like table for maximum space efficiency and retrieval speed.
---We use the constants below to index into the table. ---We use the constants below to index into the table.
---@alias oil.InternalEntry any[] ---@alias oil.InternalEntry {[1]: integer, [2]: string, [3]: oil.EntryType, [4]: nil|table}
-- Indexes into oil.InternalEntry -- Indexes into oil.InternalEntry
M.FIELD_ID = 1 M.FIELD_ID = 1

View file

@ -1,3 +1,4 @@
local log = require("oil.log")
local M = {} local M = {}
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
@ -7,6 +8,8 @@ M.is_windows = uv.os_uname().version:match("Windows")
M.is_mac = uv.os_uname().sysname == "Darwin" M.is_mac = uv.os_uname().sysname == "Darwin"
M.is_linux = not M.is_windows and not M.is_mac
---@type string ---@type string
M.sep = M.is_windows and "\\" or "/" M.sep = M.is_windows and "\\" or "/"
@ -83,8 +86,9 @@ end
M.posix_to_os_path = function(path) M.posix_to_os_path = function(path)
if M.is_windows then if M.is_windows then
if vim.startswith(path, "/") then if vim.startswith(path, "/") then
local drive, rem = path:match("^/([^/]+)/(.*)$") local drive = path:match("^/(%a+)")
return string.format("%s:\\%s", drive, rem:gsub("/", "\\")) local rem = path:sub(drive:len() + 2)
return string.format("%s:%s", drive, rem:gsub("/", "\\"))
else else
local newpath = path:gsub("/", "\\") local newpath = path:gsub("/", "\\")
return newpath return newpath
@ -113,23 +117,37 @@ end
local home_dir = assert(uv.os_homedir()) local home_dir = assert(uv.os_homedir())
---@param path string ---@param path string
---@param relative_to? string Shorten relative to this path (default cwd)
---@return string ---@return string
M.shorten_path = function(path) M.shorten_path = function(path, relative_to)
local cwd = vim.fn.getcwd() if not relative_to then
if M.is_subpath(cwd, path) then relative_to = vim.fn.getcwd()
local relative = path:sub(cwd:len() + 2) end
if relative == "" then local relpath
relative = "." if M.is_subpath(relative_to, path) then
local idx = relative_to:len() + 1
-- Trim the dividing slash if it's not included in relative_to
if not vim.endswith(relative_to, "/") and not vim.endswith(relative_to, "\\") then
idx = idx + 1
end
relpath = path:sub(idx)
if relpath == "" then
relpath = "."
end end
return relative
end end
if M.is_subpath(home_dir, path) then if M.is_subpath(home_dir, path) then
return "~" .. path:sub(home_dir:len() + 1) local homepath = "~" .. path:sub(home_dir:len() + 1)
if not relpath or homepath:len() < relpath:len() then
return homepath
end
end end
return path return relpath or path
end end
M.mkdirp = function(dir) ---@param dir string
---@param mode? integer
M.mkdirp = function(dir, mode)
mode = mode or 493
local mod = "" local mod = ""
local path = dir local path = dir
while vim.fn.isdirectory(path) == 0 do while vim.fn.isdirectory(path) == 0 do
@ -139,14 +157,14 @@ M.mkdirp = function(dir)
while mod ~= "" do while mod ~= "" do
mod = mod:sub(3) mod = mod:sub(3)
path = vim.fn.fnamemodify(dir, mod) path = vim.fn.fnamemodify(dir, mod)
uv.fs_mkdir(path, 493) uv.fs_mkdir(path, mode)
end end
end end
---@param dir string ---@param dir string
---@param cb fun(err: nil|string, entries: nil|{type: oil.EntryType, name: string}) ---@param cb fun(err: nil|string, entries: nil|{type: oil.EntryType, name: string})
M.listdir = function(dir, cb) M.listdir = function(dir, cb)
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(dir, function(open_err, fd) uv.fs_opendir(dir, function(open_err, fd)
if open_err then if open_err then
return cb(open_err) return cb(open_err)
@ -176,7 +194,7 @@ M.listdir = function(dir, cb)
end end
read_next() read_next()
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
end, 100) -- TODO do some testing for this end, 10000)
end end
---@param entry_type oil.EntryType ---@param entry_type oil.EntryType
@ -186,7 +204,7 @@ M.recursive_delete = function(entry_type, path, cb)
if entry_type ~= "directory" then if entry_type ~= "directory" then
return uv.fs_unlink(path, cb) return uv.fs_unlink(path, cb)
end end
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(path, function(open_err, fd) uv.fs_opendir(path, function(open_err, fd)
if open_err then if open_err then
return cb(open_err) return cb(open_err)
@ -200,7 +218,7 @@ M.recursive_delete = function(entry_type, path, cb)
local waiting = #entries local waiting = #entries
local complete local complete
complete = function(err2) complete = function(err2)
if err then if err2 then
complete = function() end complete = function() end
return inner_cb(err2) return inner_cb(err2)
end end
@ -228,6 +246,37 @@ M.recursive_delete = function(entry_type, path, cb)
end, 10000) end, 10000)
end 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 entry_type oil.EntryType
---@param src_path string ---@param src_path string
---@param dest_path string ---@param dest_path string
@ -245,6 +294,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
end end
if entry_type ~= "directory" then if entry_type ~= "directory" then
uv.fs_copyfile(src_path, dest_path, { excl = true }, cb) uv.fs_copyfile(src_path, dest_path, { excl = true }, cb)
move_undofile(src_path, dest_path, true)
return return
end end
uv.fs_stat(src_path, function(stat_err, src_stat) uv.fs_stat(src_path, function(stat_err, src_stat)
@ -256,7 +306,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
if mkdir_err then if mkdir_err then
return cb(mkdir_err) return cb(mkdir_err)
end end
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(src_path, function(open_err, fd) uv.fs_opendir(src_path, function(open_err, fd)
if open_err then if open_err then
return cb(open_err) return cb(open_err)
@ -270,7 +320,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
local waiting = #entries local waiting = #entries
local complete local complete
complete = function(err2) complete = function(err2)
if err then if err2 then
complete = function() end complete = function() end
return inner_cb(err2) return inner_cb(err2)
end end
@ -316,6 +366,9 @@ M.recursive_move = function(entry_type, src_path, dest_path, cb)
end end
end) end)
else else
if entry_type ~= "directory" then
move_undofile(src_path, dest_path, false)
end
cb() cb()
end end
end) end)

118
lua/oil/git.lua Normal file
View file

@ -0,0 +1,118 @@
-- integration with git operations
local fs = require("oil.fs")
local M = {}
---@param path string
---@return string|nil
M.get_root = function(path)
local git_dir = vim.fs.find(".git", { upward = true, path = path })[1]
if git_dir then
return vim.fs.dirname(git_dir)
else
return nil
end
end
---@param path string
---@param cb fun(err: nil|string)
M.add = function(path, cb)
local root = M.get_root(path)
if not root then
return cb()
end
local stderr = ""
local jid = vim.fn.jobstart({ "git", "add", path }, {
cwd = root,
stderr_buffered = true,
on_stderr = function(_, data)
stderr = table.concat(data, "\n")
end,
on_exit = function(_, code)
if code ~= 0 then
cb("Error in git add: " .. stderr)
else
cb()
end
end,
})
if jid <= 0 then
cb()
end
end
---@param path string
---@param cb fun(err: nil|string)
M.rm = function(path, cb)
local root = M.get_root(path)
if not root then
return cb()
end
local stderr = ""
local jid = vim.fn.jobstart({ "git", "rm", "-r", path }, {
cwd = root,
stderr_buffered = true,
on_stderr = function(_, data)
stderr = table.concat(data, "\n")
end,
on_exit = function(_, code)
if code ~= 0 then
stderr = vim.trim(stderr)
if stderr:match("^fatal: pathspec '.*' did not match any files$") then
cb()
else
cb("Error in git rm: " .. stderr)
end
else
cb()
end
end,
})
if jid <= 0 then
cb()
end
end
---@param entry_type oil.EntryType
---@param src_path string
---@param dest_path string
---@param cb fun(err: nil|string)
M.mv = function(entry_type, src_path, dest_path, cb)
local src_git = M.get_root(src_path)
if not src_git or src_git ~= M.get_root(dest_path) then
fs.recursive_move(entry_type, src_path, dest_path, cb)
return
end
local stderr = ""
local jid = vim.fn.jobstart({ "git", "mv", src_path, dest_path }, {
cwd = src_git,
stderr_buffered = true,
on_stderr = function(_, data)
stderr = table.concat(data, "\n")
end,
on_exit = function(_, code)
if code ~= 0 then
stderr = vim.trim(stderr)
if
stderr:match("^fatal: not under version control")
or stderr:match("^fatal: source directory is empty")
then
fs.recursive_move(entry_type, src_path, dest_path, cb)
else
cb("Error in git mv: " .. stderr)
end
else
cb()
end
end,
})
if jid <= 0 then
-- Failed to run git, fall back to normal filesystem operations
fs.recursive_move(entry_type, src_path, dest_path, cb)
end
end
return M

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
local actions = require("oil.actions") local actions = require("oil.actions")
local config = require("oil.config")
local layout = require("oil.layout") local layout = require("oil.layout")
local util = require("oil.util") local util = require("oil.util")
local M = {} local M = {}
@ -9,10 +10,27 @@ local M = {}
---@return string|nil mode ---@return string|nil mode
local function resolve(rhs) local function resolve(rhs)
if type(rhs) == "string" and vim.startswith(rhs, "actions.") then if type(rhs) == "string" and vim.startswith(rhs, "actions.") then
return resolve(actions[vim.split(rhs, ".", { plain = true })[2]]) local action_name = vim.split(rhs, ".", { plain = true })[2]
local action = actions[action_name]
if not action then
vim.notify("[oil.nvim] Unknown action name: " .. action_name, vim.log.levels.ERROR)
end
return resolve(action)
elseif type(rhs) == "table" then elseif type(rhs) == "table" then
local opts = vim.deepcopy(rhs) local opts = vim.deepcopy(rhs)
local callback = opts.callback -- 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 mode = opts.mode local mode = opts.mode
if type(rhs.callback) == "string" then if type(rhs.callback) == "string" then
local action_opts, action_mode local action_opts, action_mode
@ -20,8 +38,24 @@ local function resolve(rhs)
opts = vim.tbl_extend("keep", opts, action_opts) opts = vim.tbl_extend("keep", opts, action_opts)
mode = mode or action_mode mode = mode or action_mode
end end
-- remove all the keys that we can't pass as options to `vim.keymap.set`
opts.callback = nil opts.callback = nil
opts.mode = nil opts.mode = nil
opts[1] = nil
opts.deprecated = nil
opts.parameters = nil
if opts.opts and type(callback) == "function" then
local callback_args = opts.opts
opts.opts = nil
local orig_callback = callback
callback = function()
---@diagnostic disable-next-line: redundant-parameter
orig_callback(callback_args)
end
end
return callback, opts, mode return callback, opts, mode
else else
return rhs, {} return rhs, {}
@ -55,31 +89,30 @@ M.show_help = function(keymaps)
end end
end end
local col_left = {}
local col_desc = {}
local max_lhs = 1 local max_lhs = 1
local keymap_entries = {}
for k, rhs in pairs(keymaps) do for k, rhs in pairs(keymaps) do
local all_lhs = lhs_to_all_lhs[k] local all_lhs = lhs_to_all_lhs[k]
if all_lhs then if all_lhs then
local _, opts = resolve(rhs) local _, opts = resolve(rhs)
local keystr = table.concat(all_lhs, "/") local keystr = table.concat(all_lhs, "/")
max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr)) max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr))
table.insert(col_left, { str = keystr, all_lhs = all_lhs }) table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or "" })
table.insert(col_desc, opts.desc or "")
end end
end end
table.sort(keymap_entries, function(a, b)
return a.desc < b.desc
end)
local lines = {} local lines = {}
local highlights = {} local highlights = {}
local max_line = 1 local max_line = 1
for i = 1, #col_left do for _, entry in ipairs(keymap_entries) do
local left = col_left[i] local line = string.format(" %s %s", util.pad_align(entry.str, max_lhs, "left"), entry.desc)
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)) max_line = math.max(max_line, vim.api.nvim_strwidth(line))
table.insert(lines, line) table.insert(lines, line)
local start = 1 local start = 1
for _, key in ipairs(left.all_lhs) do for _, key in ipairs(entry.all_lhs) do
local keywidth = vim.api.nvim_strwidth(key) local keywidth = vim.api.nvim_strwidth(key)
table.insert(highlights, { "Special", #lines, start, start + keywidth }) table.insert(highlights, { "Special", #lines, start, start + keywidth })
start = start + keywidth + 1 start = start + keywidth + 1
@ -98,8 +131,8 @@ M.show_help = function(keymaps)
end end
vim.keymap.set("n", "q", "<cmd>close<CR>", { buffer = bufnr }) vim.keymap.set("n", "q", "<cmd>close<CR>", { buffer = bufnr })
vim.keymap.set("n", "<c-c>", "<cmd>close<CR>", { buffer = bufnr }) vim.keymap.set("n", "<c-c>", "<cmd>close<CR>", { buffer = bufnr })
vim.api.nvim_buf_set_option(bufnr, "modifiable", false) vim.bo[bufnr].modifiable = false
vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") vim.bo[bufnr].bufhidden = "wipe"
local editor_width = vim.o.columns local editor_width = vim.o.columns
local editor_height = layout.get_editor_height() local editor_height = layout.get_editor_height()
@ -111,7 +144,7 @@ M.show_help = function(keymaps)
height = math.min(editor_height, #lines), height = math.min(editor_height, #lines),
zindex = 150, zindex = 150,
style = "minimal", style = "minimal",
border = "rounded", border = config.keymaps_help.border,
}) })
local function close() local function close()
if vim.api.nvim_win_is_valid(winid) then if vim.api.nvim_win_is_valid(winid) then

View file

@ -1,10 +1,15 @@
local M = {} local M = {}
---@param value number
---@return boolean
local function is_float(value) local function is_float(value)
local _, p = math.modf(value) local _, p = math.modf(value)
return p ~= 0 return p ~= 0
end end
---@param value number
---@param max_value number
---@return number
local function calc_float(value, max_value) local function calc_float(value, max_value)
if value and is_float(value) then if value and is_float(value) then
return math.min(max_value, value * max_value) return math.min(max_value, value * max_value)
@ -93,6 +98,97 @@ M.calculate_height = function(desired_height, opts)
) )
end end
---@class (exact) oil.WinLayout
---@field width integer
---@field height integer
---@field row integer
---@field col integer
---@return vim.api.keyset.win_config
M.get_fullscreen_win_opts = function()
local config = require("oil.config")
local total_width = M.get_editor_width()
local total_height = M.get_editor_height()
local width = total_width - 2 * config.float.padding
if config.float.border ~= "none" then
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)
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)
end
local row = math.floor((total_height - height) / 2)
local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width
local win_opts = {
relative = "editor",
width = width,
height = height,
row = row,
col = col,
border = config.float.border,
zindex = 45,
}
return config.float.override(win_opts) or win_opts
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
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,
col = float_config.col,
row = float_config.row,
}
if vim.fn.has("nvim-0.10") == 0 then
-- read https://github.com/neovim/neovim/issues/24430 for more infos.
dim_root.col = float_config.col[vim.val_idx]
dim_root.row = float_config.row[vim.val_idx]
end
local dim_new = vim.deepcopy(dim_root)
if direction == "left" or direction == "right" then
dim_new.width = math.floor(float_config.width / 2) - math.ceil(gap / 2)
dim_root.width = dim_new.width
else
dim_new.height = math.floor(float_config.height / 2) - math.ceil(gap / 2)
dim_root.height = dim_new.height
end
if direction == "left" then
dim_root.col = dim_root.col + dim_root.width + gap
elseif direction == "right" then
dim_new.col = dim_new.col + dim_new.width + gap
elseif direction == "above" then
dim_root.row = dim_root.row + dim_root.height + gap
elseif direction == "below" then
dim_new.row = dim_new.row + dim_new.height + gap
end
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) M.calculate_dims = function(desired_width, desired_height, opts)
local width = M.calculate_width(desired_width, opts) local width = M.calculate_width(desired_width, opts)
local height = M.calculate_height(desired_height, opts) local height = M.calculate_height(desired_height, opts)

View file

@ -67,14 +67,15 @@ M.set_loading = function(bufnr, is_loading)
timers[bufnr] = vim.loop.new_timer() timers[bufnr] = vim.loop.new_timer()
local bar_iter = M.get_bar_iter({ width = width }) local bar_iter = M.get_bar_iter({ width = width })
timers[bufnr]:start( timers[bufnr]:start(
100, -- Delay the loading screen just a bit to avoid flicker 200, -- Delay the loading screen just a bit to avoid flicker
math.floor(1000 / FPS), math.floor(1000 / FPS),
vim.schedule_wrap(function() vim.schedule_wrap(function()
if not vim.api.nvim_buf_is_valid(bufnr) or not timers[bufnr] then if not vim.api.nvim_buf_is_valid(bufnr) or not timers[bufnr] then
M.set_loading(bufnr, false) M.set_loading(bufnr, false)
return return
end end
local lines = { util.lpad("Loading", math.floor(width / 2) - 3), bar_iter() } local lines =
{ util.pad_align("Loading", math.floor(width / 2) - 3, "right"), bar_iter() }
util.render_text(bufnr, lines) util.render_text(bufnr, lines)
end) end)
) )

126
lua/oil/log.lua Normal file
View file

@ -0,0 +1,126 @@
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

121
lua/oil/lsp/helpers.lua Normal file
View file

@ -0,0 +1,121 @@
local config = require("oil.config")
local fs = require("oil.fs")
local util = require("oil.util")
local workspace = require("oil.lsp.workspace")
local M = {}
---@param actions oil.Action[]
---@return fun() did_perform Call this function when the file operations have been completed
M.will_perform_file_operations = function(actions)
local moves = {}
local creates = {}
local deletes = {}
for _, action in ipairs(actions) do
if action.type == "move" then
local src_scheme, src_path = util.parse_url(action.src_url)
assert(src_path)
local src_adapter = assert(config.get_adapter_by_scheme(src_scheme))
local dest_scheme, dest_path = util.parse_url(action.dest_url)
local dest_adapter = assert(config.get_adapter_by_scheme(dest_scheme))
src_path = fs.posix_to_os_path(src_path)
dest_path = fs.posix_to_os_path(assert(dest_path))
if src_adapter.name == "files" and dest_adapter.name == "files" then
moves[src_path] = dest_path
elseif src_adapter.name == "files" then
table.insert(deletes, src_path)
elseif dest_adapter.name == "files" then
table.insert(creates, src_path)
end
elseif action.type == "create" then
local scheme, path = util.parse_url(action.url)
path = fs.posix_to_os_path(assert(path))
local adapter = assert(config.get_adapter_by_scheme(scheme))
if adapter.name == "files" then
table.insert(creates, path)
end
elseif action.type == "delete" then
local scheme, path = util.parse_url(action.url)
path = fs.posix_to_os_path(assert(path))
local adapter = assert(config.get_adapter_by_scheme(scheme))
if adapter.name == "files" then
table.insert(deletes, path)
end
elseif action.type == "copy" then
local scheme, path = util.parse_url(action.dest_url)
path = fs.posix_to_os_path(assert(path))
local adapter = assert(config.get_adapter_by_scheme(scheme))
if adapter.name == "files" then
table.insert(creates, path)
end
end
end
local buf_was_modified = {}
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
buf_was_modified[bufnr] = vim.bo[bufnr].modified
end
local edited_uris = {}
local final_err = nil
---@param edits nil|{edit: lsp.WorkspaceEdit, client_offset: string}[]
local function accum(edits, err)
final_err = final_err or err
if edits then
for _, edit in ipairs(edits) do
if edit.edit.changes then
for uri in pairs(edit.edit.changes) do
edited_uris[uri] = true
end
end
if edit.edit.documentChanges then
for _, change in ipairs(edit.edit.documentChanges) do
if change.textDocument then
edited_uris[change.textDocument.uri] = true
end
end
end
end
end
end
local timeout_ms = config.lsp_file_methods.timeout_ms
accum(workspace.will_create_files(creates, { timeout_ms = timeout_ms }))
accum(workspace.will_delete_files(deletes, { timeout_ms = timeout_ms }))
accum(workspace.will_rename_files(moves, { timeout_ms = timeout_ms }))
if final_err then
vim.notify(
string.format("[lsp] file operation error: %s", vim.inspect(final_err)),
vim.log.levels.WARN
)
end
return function()
workspace.did_create_files(creates)
workspace.did_delete_files(deletes)
workspace.did_rename_files(moves)
local autosave = config.lsp_file_methods.autosave_changes
if autosave == false then
return
end
for uri, _ in pairs(edited_uris) do
local bufnr = vim.uri_to_bufnr(uri)
local was_open = buf_was_modified[bufnr] ~= nil
local was_modified = buf_was_modified[bufnr]
local should_save = autosave == true or (autosave == "unmodified" and not was_modified)
-- Autosave changed buffers if they were not modified before
if should_save then
vim.api.nvim_buf_call(bufnr, function()
vim.cmd.update({ mods = { emsg_silent = true, noautocmd = true } })
end)
-- Delete buffers that weren't open before
if not was_open then
vim.api.nvim_buf_delete(bufnr, { force = true })
end
end
end
end
end
return M

351
lua/oil/lsp/workspace.lua Normal file
View file

@ -0,0 +1,351 @@
local fs = require("oil.fs")
local ms = require("vim.lsp.protocol").Methods
if vim.fn.has("nvim-0.10") == 0 then
ms = {
workspace_willCreateFiles = "workspace/willCreateFiles",
workspace_didCreateFiles = "workspace/didCreateFiles",
workspace_willDeleteFiles = "workspace/willDeleteFiles",
workspace_didDeleteFiles = "workspace/didDeleteFiles",
workspace_willRenameFiles = "workspace/willRenameFiles",
workspace_didRenameFiles = "workspace/didRenameFiles",
}
end
local M = {}
---@param method string
---@return vim.lsp.Client[]
local function get_clients(method)
if vim.fn.has("nvim-0.10") == 1 then
return vim.lsp.get_clients({ method = method })
else
---@diagnostic disable-next-line: deprecated
local clients = vim.lsp.get_active_clients()
return vim.tbl_filter(function(client)
return client.supports_method(method)
end, clients)
end
end
---@param glob string|vim.lpeg.Pattern
---@param path string
---@return boolean
local function match_glob(glob, path)
-- nvim-0.10 will have vim.glob.to_lpeg, so this will be a LPeg pattern
if type(glob) ~= "string" then
return glob:match(path) ~= nil
end
-- older versions fall back to glob2regpat
local pat = vim.fn.glob2regpat(glob)
local ignorecase = vim.o.ignorecase
vim.o.ignorecase = false
local ok, match = pcall(vim.fn.match, path, pat)
vim.o.ignorecase = ignorecase
if not ok then
error(match)
end
return match >= 0
end
---@param client vim.lsp.Client
---@param filters nil|lsp.FileOperationFilter[]
---@param paths string[]
---@return nil|string[]
local function get_matching_paths(client, filters, paths)
if not filters then
return nil
end
local match_fns = {}
for _, filter in ipairs(filters) do
if filter.scheme == nil or filter.scheme == "file" then
local pattern = filter.pattern
local glob = pattern.glob
local ignore_case = pattern.options and pattern.options.ignoreCase
if ignore_case then
glob = glob:lower()
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
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)
return a:len() > b:len()
end)
end
return "{" .. table.concat(filtered, ",") .. "}"
end)
glob_to_match = vim.glob.to_lpeg(glob)
end
local matches = pattern.matches
table.insert(match_fns, function(path)
local is_dir = vim.fn.isdirectory(path) == 1
if matches and ((matches == "file" and is_dir) or (matches == "folder" and not is_dir)) then
return false
end
if ignore_case then
path = path:lower()
end
return match_glob(glob_to_match, path)
end)
end
end
local function match_any_pattern(workspace, path)
local relative_path = path:sub(workspace:len() + 2)
for _, match_fn in ipairs(match_fns) do
-- The glob pattern might be relative to workspace OR absolute
if match_fn(relative_path) or match_fn(path) then
return true
end
end
return false
end
local workspace_folders = vim.tbl_map(function(folder)
return vim.uri_to_fname(folder.uri)
end, client.workspace_folders or {})
local function get_matching_workspace(path)
for _, workspace in ipairs(workspace_folders) do
if fs.is_subpath(workspace, path) then
return workspace
end
end
end
local ret = {}
for _, path in ipairs(paths) do
local workspace = get_matching_workspace(path)
if workspace and match_any_pattern(workspace, path) then
table.insert(ret, path)
end
end
if vim.tbl_isempty(ret) then
return nil
else
return ret
end
end
---@param method string The method to call
---@param capability_name string The name of the fileOperations server capability
---@param files string[] The files and folders that will be created
---@param options table|nil
---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[]
---@return nil|string|lsp.ResponseError err
local function will_file_operation(method, capability_name, files, options)
options = options or {}
local clients = get_clients(method)
local edits = {}
for _, client in ipairs(clients) do
local filters = vim.tbl_get(
client.server_capabilities,
"workspace",
"fileOperations",
capability_name,
"filters"
)
local matching_files = get_matching_paths(client, filters, files)
if matching_files then
local params = {
files = vim.tbl_map(function(file)
return {
uri = vim.uri_from_fname(file),
}
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
if result and result.result then
if options.apply_edits ~= false then
vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding)
end
table.insert(edits, { edit = result.result, offset_encoding = client.offset_encoding })
else
return nil, err or result and result.err
end
end
end
return edits
end
---@param method string The method to call
---@param capability_name string The name of the fileOperations server capability
---@param files string[] The files and folders that will be created
local function did_file_operation(method, capability_name, files)
local clients = get_clients(method)
for _, client in ipairs(clients) do
local filters = vim.tbl_get(
client.server_capabilities,
"workspace",
"fileOperations",
capability_name,
"filters"
)
local matching_files = get_matching_paths(client, filters, files)
if matching_files then
local params = {
files = vim.tbl_map(function(file)
return {
uri = vim.uri_from_fname(file),
}
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
end
end
end
--- Notify the server that the client is about to create files.
---@param files string[] The files and folders that will be created
---@param options table|nil Optional table which holds the following optional fields:
--- - timeout_ms (integer|nil, default 1000):
--- Time in milliseconds to block for rename requests.
--- - apply_edits (boolean|nil, default true):
--- Apply any workspace edits from these file operations.
---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[]
---@return nil|string|lsp.ResponseError err
function M.will_create_files(files, options)
return will_file_operation(ms.workspace_willCreateFiles, "willCreate", files, options)
end
--- Notify the server that files were created from within the client.
---@param files string[] The files and folders that will be created
function M.did_create_files(files)
did_file_operation(ms.workspace_didCreateFiles, "didCreate", files)
end
--- Notify the server that the client is about to delete files.
---@param files string[] The files and folders that will be deleted
---@param options table|nil Optional table which holds the following optional fields:
--- - timeout_ms (integer|nil, default 1000):
--- Time in milliseconds to block for rename requests.
--- - apply_edits (boolean|nil, default true):
--- Apply any workspace edits from these file operations.
---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[]
---@return nil|string|lsp.ResponseError err
function M.will_delete_files(files, options)
return will_file_operation(ms.workspace_willDeleteFiles, "willDelete", files, options)
end
--- Notify the server that files were deleted from within the client.
---@param files string[] The files and folders that were deleted
function M.did_delete_files(files)
did_file_operation(ms.workspace_didDeleteFiles, "didDelete", files)
end
--- Notify the server that the client is about to rename files.
---@param files table<string, string> Mapping of old_path -> new_path
---@param options table|nil Optional table which holds the following optional fields:
--- - timeout_ms (integer|nil, default 1000):
--- Time in milliseconds to block for rename requests.
--- - apply_edits (boolean|nil, default true):
--- Apply any workspace edits from these file operations.
---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[]
---@return nil|string|lsp.ResponseError err
function M.will_rename_files(files, options)
options = options or {}
local clients = get_clients(ms.workspace_willRenameFiles)
local edits = {}
for _, client in ipairs(clients) do
local filters = vim.tbl_get(
client.server_capabilities,
"workspace",
"fileOperations",
"willRename",
"filters"
)
local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files))
if matching_files then
local params = {
files = vim.tbl_map(function(src_file)
return {
oldUri = vim.uri_from_fname(src_file),
newUri = vim.uri_from_fname(files[src_file]),
}
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
if result and result.result then
if options.apply_edits ~= false then
vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding)
end
table.insert(edits, { edit = result.result, offset_encoding = client.offset_encoding })
else
return nil, err or result and result.err
end
end
end
return edits
end
--- Notify the server that files were renamed from within the client.
---@param files table<string, string> Mapping of old_path -> new_path
function M.did_rename_files(files)
local clients = get_clients(ms.workspace_didRenameFiles)
for _, client in ipairs(clients) do
local filters =
vim.tbl_get(client.server_capabilities, "workspace", "fileOperations", "didRename", "filters")
local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files))
if matching_files then
local params = {
files = vim.tbl_map(function(src_file)
return {
oldUri = vim.uri_from_fname(src_file),
newUri = vim.uri_from_fname(files[src_file]),
}
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
end
end
end
return M

View file

@ -1,145 +0,0 @@
local fs = require("oil.fs")
local util = require("oil.util")
local M = {}
---@param filepath string
---@param pattern lsp.FileOperationPattern
---@return boolean
local function file_matches(filepath, pattern)
local is_dir = vim.fn.isdirectory(filepath) == 1
if pattern.matches then
if (pattern.matches == "file" and is_dir) or (pattern.matches == "folder" and not is_dir) then
return false
end
end
local pat = vim.fn.glob2regpat(pattern.glob)
if vim.tbl_get(pattern, "options", "ignoreCase") then
pat = "\\c" .. pat
end
local ignorecase = vim.o.ignorecase
vim.o.ignorecase = false
local match = vim.fn.match(filepath, pat) >= 0
vim.o.ignorecase = ignorecase
return match
end
---@param filepath string
---@param filters lsp.FileOperationFilter[]
---@return boolean
local function any_match(filepath, filters)
for _, filter in ipairs(filters) do
local scheme_match = not filter.scheme or filter.scheme == "file"
if scheme_match and file_matches(filepath, filter.pattern) then
return true
end
end
return false
end
---@return nil|{src: string, dest: string}
local function get_matching_paths(client, path_pairs)
local filters =
vim.tbl_get(client.server_capabilities, "workspace", "fileOperations", "willRename", "filters")
if not filters then
return nil
end
local ret = {}
for _, pair in ipairs(path_pairs) do
if fs.is_subpath(client.config.root_dir, pair.src) then
local relative_file = pair.src:sub(client.config.root_dir:len() + 2)
if any_match(relative_file, filters) then
table.insert(ret, pair)
end
end
end
if vim.tbl_isempty(ret) then
return nil
else
return ret
end
end
---Process LSP rename in the background
---@param actions oil.MoveAction[]
M.will_rename_files = function(actions)
local path_pairs = {}
for _, action in ipairs(actions) do
local _, src_path = util.parse_url(action.src_url)
assert(src_path)
local src_file = fs.posix_to_os_path(src_path)
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
local dest_file = fs.posix_to_os_path(dest_path)
table.insert(path_pairs, { src = src_file, dest = dest_file })
end
local clients = vim.lsp.get_active_clients()
for _, client in ipairs(clients) do
local pairs = get_matching_paths(client, path_pairs)
if pairs then
client.request("workspace/willRenameFiles", {
files = vim.tbl_map(function(pair)
return {
oldUri = vim.uri_from_fname(pair.src),
newUri = vim.uri_from_fname(pair.dest),
}
end, pairs),
}, function(_, result)
if result then
vim.lsp.util.apply_workspace_edit(result, client.offset_encoding)
end
end)
end
end
end
-- LSP types from core Neovim
---A filter to describe in which file operation requests or notifications
---the server is interested in receiving.
---
---@since 3.16.0
---@class lsp.FileOperationFilter
---A Uri scheme like `file` or `untitled`.
---@field scheme? string
---The actual file operation pattern.
---@field pattern lsp.FileOperationPattern
---A pattern to describe in which file operation requests or notifications
---the server is interested in receiving.
---
---@since 3.16.0
---@class lsp.FileOperationPattern
---The glob pattern to match. Glob patterns can have the following syntax:
---- `*` to match one or more characters in a path segment
---- `?` to match on one character in a path segment
---- `**` to match any number of path segments, including none
---- `{}` to group sub patterns into an OR expression. (e.g. `**/*.{ts,js}` matches all TypeScript and JavaScript files)
---- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
---- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
---@field glob string
---Whether to match files or folders with this pattern.
---
---Matches both if undefined.
---@field matches? lsp.FileOperationPatternKind
---Additional options used during matching.
---@field options? lsp.FileOperationPatternOptions
---A pattern kind describing if a glob pattern matches a file a folder or
---both.
---
---@since 3.16.0
---@alias lsp.FileOperationPatternKind
---| "file" # file
---| "folder" # folder
---Matching options for the file operation pattern.
---
---@since 3.16.0
---@class lsp.FileOperationPatternOptions
---The pattern should be matched ignoring casing.
---@field ignoreCase? boolean
return M

View file

@ -45,11 +45,12 @@ end
---@param bufnr integer ---@param bufnr integer
---@param lines string[] ---@param lines string[]
local function render_lines(winid, bufnr, lines) local function render_lines(winid, bufnr, lines)
util.render_text( util.render_text(bufnr, lines, {
bufnr, v_align = "top",
lines, h_align = "left",
{ v_align = "top", h_align = "left", winid = winid, actions = { "[O]k", "[C]ancel" } } winid = winid,
) actions = { "[Y]es", "[N]o" },
})
end end
---@param actions oil.Action[] ---@param actions oil.Action[]
@ -81,6 +82,8 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
else else
line = adapter.render_action(action) line = adapter.render_action(action)
end end
-- We can't handle lines with newlines in them
line = line:gsub("\n", "")
table.insert(lines, line) table.insert(lines, line)
local line_width = vim.api.nvim_strwidth(line) local line_width = vim.api.nvim_strwidth(line)
if line_width > max_line_width then if line_width > max_line_width then
@ -90,7 +93,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
table.insert(lines, "") table.insert(lines, "")
-- Create the floating window -- Create the floating window
local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.preview) local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation)
local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, { local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, {
relative = "editor", relative = "editor",
width = width, width = width,
@ -99,7 +102,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
col = math.floor((layout.get_editor_width() - width) / 2), col = math.floor((layout.get_editor_width() - width) / 2),
zindex = 152, -- render on top of the floating window title zindex = 152, -- render on top of the floating window title
style = "minimal", style = "minimal",
border = config.preview.border, border = config.confirmation.border,
}) })
if not ok then if not ok then
vim.notify(string.format("Error showing oil preview window: %s", winid), vim.log.levels.ERROR) vim.notify(string.format("Error showing oil preview window: %s", winid), vim.log.levels.ERROR)
@ -107,12 +110,14 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
end end
vim.bo[bufnr].filetype = "oil_preview" vim.bo[bufnr].filetype = "oil_preview"
vim.bo[bufnr].syntax = "oil_preview" vim.bo[bufnr].syntax = "oil_preview"
for k, v in pairs(config.preview.win_options) do for k, v in pairs(config.confirmation.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
end end
render_lines(winid, bufnr, lines) render_lines(winid, bufnr, lines)
local restore_cursor = util.hide_cursor()
-- Attach autocmds and keymaps -- Attach autocmds and keymaps
local cancel local cancel
local confirm local confirm
@ -126,6 +131,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
end end
autocmds = {} autocmds = {}
vim.api.nvim_win_close(winid, true) vim.api.nvim_win_close(winid, true)
restore_cursor()
cb(value) cb(value)
end end
end end
@ -151,7 +157,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
vim.api.nvim_create_autocmd("VimResized", { vim.api.nvim_create_autocmd("VimResized", {
callback = function() callback = function()
if vim.api.nvim_win_is_valid(winid) then if vim.api.nvim_win_is_valid(winid) then
width, height = layout.calculate_dims(max_line_width, #lines, config.preview) width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation)
vim.api.nvim_win_set_config(winid, { vim.api.nvim_win_set_config(winid, {
relative = "editor", relative = "editor",
width = width, width = width,
@ -165,17 +171,22 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
end, end,
}) })
) )
for _, cancel_key in ipairs({ "q", "C", "c", "<C-c>", "<Esc>" }) do
-- We used to use [C]ancel to cancel, so preserve the old keymap
local cancel_keys = { "n", "N", "c", "C", "q", "<C-c>", "<Esc>" }
for _, cancel_key in ipairs(cancel_keys) do
vim.keymap.set("n", cancel_key, function() vim.keymap.set("n", cancel_key, function()
cancel() cancel()
end, { buffer = bufnr, nowait = true }) end, { buffer = bufnr, nowait = true })
end end
vim.keymap.set("n", "O", function()
confirm() -- We used to use [O]k to confirm, so preserve the old keymap
end, { buffer = bufnr }) local confirm_keys = { "y", "Y", "o", "O" }
vim.keymap.set("n", "o", function() for _, confirm_key in ipairs(confirm_keys) do
confirm() vim.keymap.set("n", confirm_key, function()
end, { buffer = bufnr }) confirm()
end, { buffer = bufnr, nowait = true })
end
end) end)
return M return M

View file

@ -3,12 +3,12 @@ local Trie = require("oil.mutator.trie")
local cache = require("oil.cache") local cache = require("oil.cache")
local columns = require("oil.columns") local columns = require("oil.columns")
local config = require("oil.config") local config = require("oil.config")
local confirmation = require("oil.mutator.confirmation")
local constants = require("oil.constants") local constants = require("oil.constants")
local lsp_helpers = require("oil.lsp_helpers") local fs = require("oil.fs")
local lsp_helpers = require("oil.lsp.helpers")
local oil = require("oil") local oil = require("oil")
local parser = require("oil.mutator.parser") local parser = require("oil.mutator.parser")
local pathutil = require("oil.pathutil")
local preview = require("oil.mutator.preview")
local util = require("oil.util") local util = require("oil.util")
local view = require("oil.view") local view = require("oil.view")
local M = {} local M = {}
@ -54,6 +54,7 @@ M.create_actions_from_diffs = function(all_diffs)
---@type oil.Action[] ---@type oil.Action[]
local actions = {} local actions = {}
---@type table<integer, oil.Diff[]>
local diff_by_id = setmetatable({}, { local diff_by_id = setmetatable({}, {
__index = function(t, key) __index = function(t, key)
local list = {} local list = {}
@ -61,8 +62,30 @@ M.create_actions_from_diffs = function(all_diffs)
return list return list
end, end,
}) })
-- To deduplicate create actions
-- This can happen when creating deep nested files e.g.
-- > foo/bar/a.txt
-- > foo/bar/b.txt
local seen_creates = {}
---@param action oil.Action
local function add_action(action)
local adapter = assert(config.get_adapter_by_scheme(action.dest_url or action.url))
if not adapter.filter_action or adapter.filter_action(action) then
if action.type == "create" then
if seen_creates[action.url] then
return
else
seen_creates[action.url] = true
end
end
table.insert(actions, action)
end
end
for bufnr, diffs in pairs(all_diffs) do for bufnr, diffs in pairs(all_diffs) do
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if not adapter then if not adapter then
error("Missing adapter") error("Missing adapter")
end end
@ -71,13 +94,14 @@ M.create_actions_from_diffs = function(all_diffs)
if diff.type == "new" then if diff.type == "new" then
if diff.id then if diff.id then
local by_id = diff_by_id[diff.id] local by_id = diff_by_id[diff.id]
-- FIXME this is kind of a hack. We shouldn't be setting undocumented fields on the diff ---HACK: set the destination on this diff for use later
---@diagnostic disable-next-line: inject-field ---@diagnostic disable-next-line: inject-field
diff.dest = parent_url .. diff.name diff.dest = parent_url .. diff.name
table.insert(by_id, diff) table.insert(by_id, diff)
else else
-- Parse nested files like foo/bar/baz -- Parse nested files like foo/bar/baz
local pieces = vim.split(diff.name, "/") local path_sep = fs.is_windows and "[/\\]" or "/"
local pieces = vim.split(diff.name, path_sep)
local url = parent_url:gsub("/$", "") local url = parent_url:gsub("/$", "")
for i, v in ipairs(pieces) do for i, v in ipairs(pieces) do
local is_last = i == #pieces local is_last = i == #pieces
@ -87,7 +111,7 @@ M.create_actions_from_diffs = function(all_diffs)
-- Parse alternations like foo.{js,test.js} -- Parse alternations like foo.{js,test.js}
for _, alt in ipairs(vim.split(alternation, ",")) do for _, alt in ipairs(vim.split(alternation, ",")) do
local alt_url = url .. "/" .. v:gsub("{[^}]+}", alt) local alt_url = url .. "/" .. v:gsub("{[^}]+}", alt)
table.insert(actions, { add_action({
type = "create", type = "create",
url = alt_url, url = alt_url,
entry_type = entry_type, entry_type = entry_type,
@ -96,7 +120,7 @@ M.create_actions_from_diffs = function(all_diffs)
end end
else else
url = url .. "/" .. v url = url .. "/" .. v
table.insert(actions, { add_action({
type = "create", type = "create",
url = url, url = url,
entry_type = entry_type, entry_type = entry_type,
@ -106,7 +130,7 @@ M.create_actions_from_diffs = function(all_diffs)
end end
end end
elseif diff.type == "change" then elseif diff.type == "change" then
table.insert(actions, { add_action({
type = "change", type = "change",
url = parent_url .. diff.name, url = parent_url .. diff.name,
entry_type = diff.entry_type, entry_type = diff.entry_type,
@ -115,8 +139,10 @@ M.create_actions_from_diffs = function(all_diffs)
}) })
else else
local by_id = diff_by_id[diff.id] 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 by_id.has_delete = true
-- Don't insert the delete. We already know that there is a delete because of the presense -- 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. -- in the diff_by_id map. The list will only include the 'new' diffs.
end end
end end
@ -127,21 +153,25 @@ M.create_actions_from_diffs = function(all_diffs)
if not entry then if not entry then
error(string.format("Could not find entry %d", id)) error(string.format("Could not find entry %d", id))
end end
---HACK: access the has_delete field on the list-like table of diffs
---@diagnostic disable-next-line: undefined-field
if diffs.has_delete then if diffs.has_delete then
local has_create = #diffs > 0 local has_create = #diffs > 0
if has_create then if has_create then
-- MOVE (+ optional copies) when has both creates and delete -- MOVE (+ optional copies) when has both creates and delete
for i, diff in ipairs(diffs) do for i, diff in ipairs(diffs) do
table.insert(actions, { add_action({
type = i == #diffs and "move" or "copy", type = i == #diffs and "move" or "copy",
entry_type = entry[FIELD_TYPE], entry_type = entry[FIELD_TYPE],
---HACK: access the dest field we set above
---@diagnostic disable-next-line: undefined-field
dest_url = diff.dest, dest_url = diff.dest,
src_url = cache.get_parent_url(id) .. entry[FIELD_NAME], src_url = cache.get_parent_url(id) .. entry[FIELD_NAME],
}) })
end end
else else
-- DELETE when no create -- DELETE when no create
table.insert(actions, { add_action({
type = "delete", type = "delete",
entry_type = entry[FIELD_TYPE], entry_type = entry[FIELD_TYPE],
url = cache.get_parent_url(id) .. entry[FIELD_NAME], url = cache.get_parent_url(id) .. entry[FIELD_NAME],
@ -150,10 +180,12 @@ M.create_actions_from_diffs = function(all_diffs)
else else
-- COPY when create but no delete -- COPY when create but no delete
for _, diff in ipairs(diffs) do for _, diff in ipairs(diffs) do
table.insert(actions, { add_action({
type = "copy", type = "copy",
entry_type = entry[FIELD_TYPE], entry_type = entry[FIELD_TYPE],
src_url = cache.get_parent_url(id) .. entry[FIELD_NAME], src_url = cache.get_parent_url(id) .. entry[FIELD_NAME],
---HACK: access the dest field we set above
---@diagnostic disable-next-line: undefined-field
dest_url = diff.dest, dest_url = diff.dest,
}) })
end end
@ -221,11 +253,10 @@ M.enforce_action_order = function(actions)
-- Process children before moving -- Process children before moving
-- e.g. NEW /a/b BEFORE MOVE /a -> /b -- e.g. NEW /a/b BEFORE MOVE /a -> /b
dest_trie:accum_children_of(action.src_url, ret) dest_trie:accum_children_of(action.src_url, ret)
-- Copy children before moving parent dir -- Process children before moving parent dir
-- e.g. COPY /a/b -> /b BEFORE MOVE /a -> /d -- e.g. COPY /a/b -> /b BEFORE MOVE /a -> /d
src_trie:accum_children_of(action.src_url, ret, function(a) -- e.g. CHANGE /a/b BEFORE MOVE /a -> /d
return a.type == "copy" src_trie:accum_children_of(action.src_url, ret)
end)
-- Process remove path before moving to new path -- Process remove path before moving to new path
-- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a -- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a) src_trie:accum_actions_at(action.dest_url, ret, function(a)
@ -350,52 +381,28 @@ M.enforce_action_order = function(actions)
return ret return ret
end end
local progress
---@param actions oil.Action[] ---@param actions oil.Action[]
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.process_actions = function(actions, cb) M.process_actions = function(actions, cb)
-- convert delete actions to move-to-trash vim.api.nvim_exec_autocmds(
local trash_url = config.get_trash_url() "User",
if trash_url then { pattern = "OilActionsPre", modeline = false, data = { actions = actions } }
for i, v in ipairs(actions) do )
if v.type == "delete" then
local scheme, path = util.parse_url(v.url) local did_complete = nil
if config.adapters[scheme] == "files" then if config.lsp_file_methods.enabled then
assert(path) did_complete = lsp_helpers.will_perform_file_operations(actions)
---@type oil.MoveAction
local move_action = {
type = "move",
src_url = v.url,
entry_type = v.entry_type,
dest_url = trash_url .. "/" .. pathutil.basename(path) .. string.format(
"_%06d",
math.random(999999)
),
}
actions[i] = move_action
end
end
end
end end
-- send all renames to LSP servers -- Convert some cross-adapter moves to a copy + delete
local moves = {}
for _, action in ipairs(actions) do for _, action in ipairs(actions) do
if action.type == "move" then if action.type == "move" then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) local _, cross_action = util.get_adapter_for_action(action)
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) -- Only do the conversion if the cross-adapter support is "copy"
if src_adapter.name == "files" and dest_adapter.name == "files" then if cross_action == "copy" then
table.insert(moves, action) ---@diagnostic disable-next-line: assign-type-mismatch
end
end
end
lsp_helpers.will_rename_files(moves)
-- Convert cross-adapter moves to a copy + delete
for _, action in ipairs(actions) do
if action.type == "move" then
local src_scheme = util.parse_url(action.src_url)
local dest_scheme = util.parse_url(action.dest_url)
if src_scheme ~= dest_scheme then
action.type = "copy" action.type = "copy"
table.insert(actions, { table.insert(actions, {
type = "delete", type = "delete",
@ -407,12 +414,17 @@ M.process_actions = function(actions, cb)
end end
local finished = false local finished = false
local progress = Progress.new() progress = Progress.new()
local function finish(...) local function finish(err)
if not finished then if not finished then
finished = true finished = true
progress:close() progress:close()
cb(...) progress = nil
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilActionsPost", modeline = false, data = { err = err, actions = actions } }
)
cb(err)
end end
end end
@ -436,6 +448,9 @@ M.process_actions = function(actions, cb)
return return
end end
if idx > #actions then if idx > #actions then
if did_complete then
did_complete()
end
finish() finish()
return return
end end
@ -467,18 +482,34 @@ M.process_actions = function(actions, cb)
next_action() next_action()
end end
M.show_progress = function()
if progress then
progress:restore()
end
end
local mutation_in_progress = false local mutation_in_progress = false
---@return boolean
M.is_mutating = function()
return mutation_in_progress
end
---@param confirm nil|boolean ---@param confirm nil|boolean
M.try_write_changes = function(confirm) ---@param cb? fun(err: nil|string)
M.try_write_changes = function(confirm, cb)
if not cb then
cb = function(_err) end
end
if mutation_in_progress then if mutation_in_progress then
error("Cannot perform mutation when already in progress") cb("Cannot perform mutation when already in progress")
return return
end end
local current_buf = vim.api.nvim_get_current_buf() local current_buf = vim.api.nvim_get_current_buf()
local was_modified = vim.bo.modified local was_modified = vim.bo.modified
local buffers = view.get_all_buffers() local buffers = view.get_all_buffers()
local all_diffs = {} local all_diffs = {}
---@type table<integer, oil.ParseError[]>
local all_errors = {} local all_errors = {}
mutation_in_progress = true mutation_in_progress = true
@ -488,6 +519,10 @@ M.try_write_changes = function(confirm)
if vim.bo[bufnr].modified then if vim.bo[bufnr].modified then
local diffs, errors = parser.parse(bufnr) local diffs, errors = parser.parse(bufnr)
all_diffs[bufnr] = diffs all_diffs[bufnr] = diffs
local adapter = assert(util.get_adapter(bufnr, true))
if adapter.filter_error then
errors = vim.tbl_filter(adapter.filter_error, errors)
end
if not vim.tbl_isempty(errors) then if not vim.tbl_isempty(errors) then
all_errors[bufnr] = errors all_errors[bufnr] = errors
end end
@ -505,7 +540,6 @@ M.try_write_changes = function(confirm)
local ns = vim.api.nvim_create_namespace("Oil") local ns = vim.api.nvim_create_namespace("Oil")
vim.diagnostic.reset(ns) vim.diagnostic.reset(ns)
if not vim.tbl_isempty(all_errors) then if not vim.tbl_isempty(all_errors) then
vim.notify("Error parsing oil buffers", vim.log.levels.ERROR)
for bufnr, errors in pairs(all_errors) do for bufnr, errors in pairs(all_errors) do
vim.diagnostic.set(ns, bufnr, errors) vim.diagnostic.set(ns, bufnr, errors)
end end
@ -519,18 +553,27 @@ M.try_write_changes = function(confirm)
{ all_errors[curbuf][1].lnum + 1, all_errors[curbuf][1].col } { all_errors[curbuf][1].lnum + 1, all_errors[curbuf][1].col }
) )
else else
---@diagnostic disable-next-line: param-type-mismatch local bufnr, errs = next(all_errors)
local bufnr, errs = next(pairs(all_errors)) assert(bufnr)
vim.api.nvim_win_set_buf(0, bufnr) assert(errs)
pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col }) -- 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)
end end
return unlock() unlock()
cb("Error parsing oil buffers")
return
end end
local actions = M.create_actions_from_diffs(all_diffs) local actions = M.create_actions_from_diffs(all_diffs)
preview.show(actions, confirm, function(proceed) confirmation.show(actions, confirm, function(proceed)
if not proceed then if not proceed then
return unlock() unlock()
cb("Canceled")
return
end end
M.process_actions( M.process_actions(
@ -538,8 +581,10 @@ M.try_write_changes = function(confirm)
vim.schedule_wrap(function(err) vim.schedule_wrap(function(err)
view.unlock_buffers() view.unlock_buffers()
if err then if err then
vim.notify(string.format("[oil] Error applying actions: %s", err), vim.log.levels.ERROR) err = string.format("[oil] Error applying actions: %s", err)
view.rerender_all_oil_buffers({ preserve_undo = false }) view.rerender_all_oil_buffers(nil, function()
cb(err)
end)
else else
local current_entry = oil.get_cursor_entry() local current_entry = oil.get_cursor_entry()
if current_entry then if current_entry then
@ -549,7 +594,13 @@ M.try_write_changes = function(confirm)
vim.split(current_entry.parsed_name or current_entry.name, "/")[1] vim.split(current_entry.parsed_name or current_entry.name, "/")[1]
) )
end end
view.rerender_all_oil_buffers({ preserve_undo = M.trash }) view.rerender_all_oil_buffers(nil, function(render_err)
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilMutationComplete", modeline = false }
)
cb(render_err)
end)
end end
mutation_in_progress = false mutation_in_progress = false
end) end)

View file

@ -25,7 +25,7 @@ local FIELD_META = constants.FIELD_META
---@field type "delete" ---@field type "delete"
---@field name string ---@field name string
---@field id integer ---@field id integer
---
---@class (exact) oil.DiffChange ---@class (exact) oil.DiffChange
---@field type "change" ---@field type "change"
---@field entry_type oil.EntryType ---@field entry_type oil.EntryType
@ -37,7 +37,7 @@ local FIELD_META = constants.FIELD_META
---@return string ---@return string
---@return boolean ---@return boolean
local function parsedir(name) local function parsedir(name)
local isdir = vim.endswith(name, "/") local isdir = vim.endswith(name, "/") or (fs.is_windows and vim.endswith(name, "\\"))
if isdir then if isdir then
name = name:sub(1, name:len() - 1) name = name:sub(1, name:len() - 1)
end end
@ -95,8 +95,8 @@ M.parse_line = function(adapter, line, column_defs)
local name = util.split_config(def) local name = util.split_config(def)
local range = { start } local range = { start }
local start_len = string.len(rem) local start_len = string.len(rem)
value, rem = columns.parse_col(adapter, rem, def) value, rem = columns.parse_col(adapter, assert(rem), def)
if not value or not rem then if not rem then
return nil, string.format("Parsing %s failed", name) return nil, string.format("Parsing %s failed", name)
end end
ret[name] = value ret[name] = value
@ -142,14 +142,21 @@ M.parse_line = function(adapter, line, column_defs)
return { data = ret, entry = entry, ranges = ranges } return { data = ret, entry = entry, ranges = ranges }
end end
---@class (exact) oil.ParseError
---@field lnum integer
---@field col integer
---@field message string
---@param bufnr integer ---@param bufnr integer
---@return oil.Diff[] ---@return oil.Diff[] diffs
---@return table[] Parsing errors ---@return oil.ParseError[] errors Parsing errors
M.parse = function(bufnr) M.parse = function(bufnr)
---@type oil.Diff[]
local diffs = {} local diffs = {}
---@type oil.ParseError[]
local errors = {} local errors = {}
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if not adapter then if not adapter then
table.insert(errors, { table.insert(errors, {
lnum = 0, lnum = 0,
@ -158,15 +165,19 @@ M.parse = function(bufnr)
}) })
return diffs, errors return diffs, errors
end end
local scheme, path = util.parse_url(bufname)
local parent_url = scheme .. path
local column_defs = columns.get_supported_columns(adapter)
local children = cache.list_url(parent_url)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
local scheme, path = util.parse_url(bufname)
local column_defs = columns.get_supported_columns(adapter)
local parent_url = scheme .. path
local children = cache.list_url(parent_url)
-- map from name to entry ID for all entries previously in the buffer
---@type table<string, integer>
local original_entries = {} local original_entries = {}
for _, child in pairs(children) do for _, child in pairs(children) do
if view.should_display(child, bufnr) then local name = child[FIELD_NAME]
original_entries[child[FIELD_NAME]] = child[FIELD_ID] if view.should_display(name, bufnr) then
original_entries[name] = child[FIELD_ID]
end end
end end
local seen_names = {} local seen_names = {}
@ -176,109 +187,124 @@ M.parse = function(bufnr)
name = name:lower() name = name:lower()
end end
if seen_names[name] then if seen_names[name] then
table.insert(errors, { message = "Duplicate filename", lnum = i - 1, col = 0 }) table.insert(errors, { message = "Duplicate filename", lnum = i - 1, end_lnum = i, col = 0 })
else else
seen_names[name] = true seen_names[name] = true
end end
end end
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
if line:match("^/%d+") then -- hack to be compatible with Lua 5.1
local result, err = M.parse_line(adapter, line, column_defs) -- use return instead of goto
if not result or err then (function()
table.insert(errors, { if line:match("^/%d+") then
message = err, -- Parse the line for an existing entry
lnum = i - 1, local result, err = M.parse_line(adapter, line, column_defs)
col = 0, if not result or err then
}) table.insert(errors, {
goto continue message = err,
end lnum = i - 1,
local parsed_entry = result.data end_lnum = i,
local entry = result.entry col = 0,
if not parsed_entry.name or parsed_entry.name:match("/") or not entry then
local message
if not parsed_entry.name then
message = "No filename found"
elseif not entry then
message = "Could not find existing entry (was the ID changed?)"
else
message = "Filename cannot contain '/'"
end
table.insert(errors, {
message = message,
lnum = i - 1,
col = 0,
})
goto continue
end
check_dupe(parsed_entry.name, i)
local meta = entry[FIELD_META]
if original_entries[parsed_entry.name] == parsed_entry.id then
if entry[FIELD_TYPE] == "link" and not compare_link_target(meta, parsed_entry) then
table.insert(diffs, {
type = "new",
name = parsed_entry.name,
entry_type = "link",
link = parsed_entry.link_target,
}) })
elseif entry[FIELD_TYPE] ~= parsed_entry._type then return
elseif result.data.id == 0 then
-- Ignore entries with ID 0 (typically the "../" entry)
return
end
local parsed_entry = result.data
local entry = result.entry
local err_message
if not parsed_entry.name then
err_message = "No filename found"
elseif not entry then
err_message = "Could not find existing entry (was the ID changed?)"
elseif parsed_entry.name:match("/") or parsed_entry.name:match(fs.sep) then
err_message = "Filename cannot contain path separator"
end
if err_message then
table.insert(errors, {
message = err_message,
lnum = i - 1,
end_lnum = i,
col = 0,
})
return
end
assert(entry)
check_dupe(parsed_entry.name, i)
local meta = entry[FIELD_META]
if original_entries[parsed_entry.name] == parsed_entry.id then
if entry[FIELD_TYPE] == "link" and not compare_link_target(meta, parsed_entry) then
table.insert(diffs, {
type = "new",
name = parsed_entry.name,
entry_type = "link",
link = parsed_entry.link_target,
})
elseif entry[FIELD_TYPE] ~= parsed_entry._type then
table.insert(diffs, {
type = "new",
name = parsed_entry.name,
entry_type = parsed_entry._type,
})
else
original_entries[parsed_entry.name] = nil
end
else
table.insert(diffs, { table.insert(diffs, {
type = "new", type = "new",
name = parsed_entry.name, name = parsed_entry.name,
entry_type = parsed_entry._type, entry_type = parsed_entry._type,
id = parsed_entry.id,
link = parsed_entry.link_target,
}) })
else end
original_entries[parsed_entry.name] = nil
for _, col_def in ipairs(column_defs) do
local col_name = util.split_config(col_def)
if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then
table.insert(diffs, {
type = "change",
name = parsed_entry.name,
entry_type = entry[FIELD_TYPE],
column = col_name,
value = parsed_entry[col_name],
})
end
end end
else else
table.insert(diffs, { -- Parse a new entry
type = "new", local name, isdir = parsedir(vim.trim(line))
name = parsed_entry.name, if vim.startswith(name, "/") then
entry_type = parsed_entry._type, table.insert(errors, {
id = parsed_entry.id, message = "Paths cannot start with '/'",
link = parsed_entry.link_target, lnum = i - 1,
}) end_lnum = i,
end col = 0,
})
for _, col_def in ipairs(column_defs) do return
local col_name = util.split_config(col_def) end
if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then if name ~= "" then
local link_pieces = vim.split(name, " -> ", { plain = true })
local entry_type = isdir and "directory" or "file"
local link
if #link_pieces == 2 then
entry_type = "link"
name, link = unpack(link_pieces)
end
check_dupe(name, i)
table.insert(diffs, { table.insert(diffs, {
type = "change", type = "new",
name = parsed_entry.name, name = name,
entry_type = entry[FIELD_TYPE], entry_type = entry_type,
column = col_name, link = link,
value = parsed_entry[col_name],
}) })
end end
end end
else end)()
local name, isdir = parsedir(vim.trim(line))
if vim.startswith(name, "/") then
table.insert(errors, {
message = "Paths cannot start with '/'",
lnum = i - 1,
col = 0,
})
goto continue
end
if name ~= "" then
local link_pieces = vim.split(name, " -> ", { plain = true })
local entry_type = isdir and "directory" or "file"
local link
if #link_pieces == 2 then
entry_type = "link"
name, link = unpack(link_pieces)
end
check_dupe(name, i)
table.insert(diffs, {
type = "new",
name = name,
entry_type = entry_type,
link = link,
})
end
end
::continue::
end end
for name, child_id in pairs(original_entries) do for name, child_id in pairs(original_entries) do

View file

@ -8,13 +8,11 @@ local Progress = {}
local FPS = 20 local FPS = 20
function Progress.new() function Progress.new()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe"
return setmetatable({ return setmetatable({
lines = { "", "" }, lines = { "", "" },
count = "", count = "",
spinner = "", spinner = "",
bufnr = bufnr, bufnr = nil,
winid = nil, winid = nil,
min_bufnr = nil, min_bufnr = nil,
min_winid = nil, min_winid = nil,
@ -25,6 +23,15 @@ function Progress.new()
}) })
end end
---@private
---@return boolean
function Progress:is_minimized()
return not self.closing
and not self.bufnr
and self.min_bufnr
and vim.api.nvim_buf_is_valid(self.min_bufnr)
end
---@param opts nil|table ---@param opts nil|table
--- cancel fun() --- cancel fun()
function Progress:show(opts) function Progress:show(opts)
@ -32,20 +39,24 @@ function Progress:show(opts)
if self.winid and vim.api.nvim_win_is_valid(self.winid) then if self.winid and vim.api.nvim_win_is_valid(self.winid) then
return return
end end
self.closing = false local bufnr = vim.api.nvim_create_buf(false, true)
self.cancel = opts.cancel vim.bo[bufnr].bufhidden = "wipe"
self.bufnr = bufnr
self.cancel = opts.cancel or self.cancel
local loading_iter = loading.get_bar_iter() local loading_iter = loading.get_bar_iter()
local spinner = loading.get_iter("dots") local spinner = loading.get_iter("dots")
self.timer = vim.loop.new_timer() if not self.timer then
self.timer:start( self.timer = vim.loop.new_timer()
0, self.timer:start(
math.floor(1000 / FPS), 0,
vim.schedule_wrap(function() math.floor(1000 / FPS),
self.lines[2] = string.format("%s %s", self.count, loading_iter()) vim.schedule_wrap(function()
self.spinner = spinner() self.lines[2] = string.format("%s %s", self.count, loading_iter())
self:_render() self.spinner = spinner()
end) self:_render()
) end)
)
end
local width, height = layout.calculate_dims(120, 10, config.progress) local width, height = layout.calculate_dims(120, 10, config.progress)
self.winid = vim.api.nvim_open_win(self.bufnr, true, { self.winid = vim.api.nvim_open_win(self.bufnr, true, {
relative = "editor", relative = "editor",
@ -58,7 +69,7 @@ function Progress:show(opts)
border = config.progress.border, border = config.progress.border,
}) })
vim.bo[self.bufnr].filetype = "oil_progress" vim.bo[self.bufnr].filetype = "oil_progress"
for k, v in pairs(config.preview.win_options) do for k, v in pairs(config.progress.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = self.winid }) vim.api.nvim_set_option_value(k, v, { scope = "local", win = self.winid })
end end
table.insert( table.insert(
@ -89,6 +100,16 @@ function Progress:show(opts)
vim.keymap.set("n", "M", minimize, { buffer = self.bufnr, nowait = true }) vim.keymap.set("n", "M", minimize, { buffer = self.bufnr, nowait = true })
end end
function Progress:restore()
if self.closing then
return
elseif not self:is_minimized() then
error("Cannot restore progress window: not minimized")
end
self:_cleanup_minimized_win()
self:show()
end
function Progress:_render() function Progress:_render()
if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then
util.render_text( util.render_text(
@ -139,6 +160,14 @@ function Progress:_cleanup_main_win()
self.bufnr = nil self.bufnr = nil
end end
function Progress:_cleanup_minimized_win()
if self.min_winid and vim.api.nvim_win_is_valid(self.min_winid) then
vim.api.nvim_win_close(self.min_winid, true)
end
self.min_winid = nil
self.min_bufnr = nil
end
function Progress:minimize() function Progress:minimize()
if self.closing then if self.closing then
return return
@ -160,6 +189,7 @@ function Progress:minimize()
self.min_bufnr = bufnr self.min_bufnr = bufnr
self.min_winid = winid self.min_winid = winid
self:_render() self:_render()
vim.notify_once("Restore progress window with :Oil --progress")
end end
---@param action oil.Action ---@param action oil.Action
@ -187,11 +217,7 @@ function Progress:close()
self.timer = nil self.timer = nil
end end
self:_cleanup_main_win() self:_cleanup_main_win()
if self.min_winid and vim.api.nvim_win_is_valid(self.min_winid) then self:_cleanup_minimized_win()
vim.api.nvim_win_close(self.min_winid, true)
end
self.min_winid = nil
self.min_bufnr = nil
end end
return Progress return Progress

View file

@ -7,6 +7,7 @@ local Trie = {}
---@return oil.Trie ---@return oil.Trie
Trie.new = function() Trie.new = function()
---@type oil.Trie
return setmetatable({ return setmetatable({
root = { values = {}, children = {} }, root = { values = {}, children = {} },
}, { }, {

View file

@ -1,16 +1,8 @@
local fs = require("oil.fs")
local M = {} local M = {}
---@param path string ---@param path string
---@return string ---@return string
M.parent = function(path) M.parent = function(path)
-- Do I love this hack? No I do not.
-- Does it work? Yes. Mostly. For now.
if fs.is_windows then
if path:match("^/%a+/?$") then
return path
end
end
if path == "/" then if path == "/" then
return "/" return "/"
elseif path == "" then elseif path == "" then

37
lua/oil/ringbuf.lua Normal file
View file

@ -0,0 +1,37 @@
---@class oil.Ringbuf
---@field private size integer
---@field private tail integer
---@field private buf string[]
local Ringbuf = {}
function Ringbuf.new(size)
local self = setmetatable({
size = size,
buf = {},
tail = 0,
}, { __index = Ringbuf })
return self
end
---@param val string
function Ringbuf:push(val)
self.tail = self.tail + 1
if self.tail > self.size then
self.tail = 1
end
self.buf[self.tail] = val
end
---@return string
function Ringbuf:as_str()
local postfix = ""
for i = 1, self.tail, 1 do
postfix = postfix .. self.buf[i]
end
local prefix = ""
for i = self.tail + 1, #self.buf, 1 do
prefix = prefix .. self.buf[i]
end
return prefix .. postfix
end
return Ringbuf

View file

@ -26,7 +26,8 @@ M.run = function(cmd, opts, callback)
if err == "" then if err == "" then
err = "Unknown error" err = "Unknown error"
end end
callback(err) local cmd_str = type(cmd) == "string" and cmd or table.concat(cmd, " ")
callback(string.format("Error running command '%s'\n%s", cmd_str, err))
end end
end), end),
}) })

View file

@ -8,6 +8,8 @@ local FIELD_NAME = constants.FIELD_NAME
local FIELD_TYPE = constants.FIELD_TYPE local FIELD_TYPE = constants.FIELD_TYPE
local FIELD_META = constants.FIELD_META local FIELD_META = constants.FIELD_META
---@alias oil.IconProvider fun(type: string, name: string, conf: table?): (icon: string, hl: string)
---@param url string ---@param url string
---@return nil|string ---@return nil|string
---@return nil|string ---@return nil|string
@ -19,50 +21,67 @@ end
---@param filename string ---@param filename string
---@return string ---@return string
M.escape_filename = function(filename) M.escape_filename = function(filename)
local ret = filename:gsub("([%%#$])", "\\%1") local ret = vim.fn.fnameescape(filename)
return ret return ret
end end
local _url_escape_chars = { local _url_escape_to_char = {
[" "] = "%20", ["20"] = " ",
["$"] = "%24", ["22"] = "",
["&"] = "%26", ["23"] = "#",
["`"] = "%60", ["24"] = "$",
[":"] = "%3A", ["25"] = "%",
["<"] = "%3C", ["26"] = "&",
["="] = "%3D", ["27"] = "",
[">"] = "%3E", ["2B"] = "+",
["?"] = "%3F", ["2C"] = ",",
["["] = "%5B", ["2F"] = "/",
["\\"] = "%5C", ["3A"] = ":",
["]"] = "%5D", ["3B"] = ";",
["^"] = "%5E", ["3C"] = "<",
["{"] = "%7B", ["3D"] = "=",
["|"] = "%7C", ["3E"] = ">",
["}"] = "%7D", ["3F"] = "?",
["~"] = "%7E", ["40"] = "@",
[""] = "%22", ["5B"] = "[",
[""] = "%27", ["5C"] = "\\",
["+"] = "%2B", ["5D"] = "]",
[","] = "%2C", ["5E"] = "^",
["#"] = "%23", ["60"] = "`",
["%"] = "%25", ["7B"] = "{",
["@"] = "%40", ["7C"] = "|",
["/"] = "%2F", ["7D"] = "}",
[";"] = "%3B", ["7E"] = "~",
} }
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 ---@param string string
---@return string ---@return string
M.url_escape = function(string) M.url_escape = function(string)
return (string:gsub(".", _url_escape_chars)) 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)
)
end end
---@param bufnr integer ---@param bufnr integer
---@param silent? boolean
---@return nil|oil.Adapter ---@return nil|oil.Adapter
M.get_adapter = function(bufnr) M.get_adapter = function(bufnr, silent)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = config.get_adapter_by_scheme(bufname) local adapter = config.get_adapter_by_scheme(bufname)
if not adapter then if not adapter and not silent then
vim.notify_once( vim.notify_once(
string.format("[oil] could not find adapter for buffer '%s://'", bufname), string.format("[oil] could not find adapter for buffer '%s://'", bufname),
vim.log.levels.ERROR vim.log.levels.ERROR
@ -72,34 +91,28 @@ M.get_adapter = function(bufnr)
end end
---@param text string ---@param text string
---@param length nil|integer ---@param width integer|nil
---@return string ---@param align oil.ColumnAlign
M.rpad = function(text, length) ---@return string padded_text
if not length then ---@return integer left_padding
return text M.pad_align = function(text, width, align)
if not width then
return text, 0
end end
local textlen = vim.api.nvim_strwidth(text) local text_width = vim.api.nvim_strwidth(text)
local delta = length - textlen local total_pad = width - text_width
if delta > 0 then if total_pad <= 0 then
return text .. string.rep(" ", delta) return text, 0
else
return text
end end
end
---@param text string if align == "right" then
---@param length nil|integer return string.rep(" ", total_pad) .. text, total_pad
---@return string elseif align == "center" then
M.lpad = function(text, length) local left_pad = math.floor(total_pad / 2)
if not length then local right_pad = total_pad - left_pad
return text return string.rep(" ", left_pad) .. text .. string.rep(" ", right_pad), left_pad
end
local textlen = vim.api.nvim_strwidth(text)
local delta = length - textlen
if delta > 0 then
return string.rep(" ", delta) .. text
else else
return text return text .. string.rep(" ", total_pad), 0
end end
end end
@ -155,8 +168,10 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- This will fail if the dest buf name already exists -- This will fail if the dest buf name already exists
local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name) local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name)
if ok then if ok then
-- Renaming the buffer creates a new buffer with the old name. Find it and delete it. -- Renaming the buffer creates a new buffer with the old name.
vim.api.nvim_buf_delete(vim.fn.bufadd(bufname), {}) -- 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), {})
if altbuf and vim.api.nvim_buf_is_valid(altbuf) then if altbuf and vim.api.nvim_buf_is_valid(altbuf) then
vim.fn.setreg("#", altbuf) vim.fn.setreg("#", altbuf)
end end
@ -193,6 +208,18 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- Try to delete, but don't if the buffer has changes -- Try to delete, but don't if the buffer has changes
pcall(vim.api.nvim_buf_delete, src_bufnr, {}) pcall(vim.api.nvim_buf_delete, src_bufnr, {})
end 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) end)
return true return true
end end
@ -281,11 +308,15 @@ M.split_config = function(name_or_config)
end end
end end
---@alias oil.ColumnAlign "left"|"center"|"right"
---@param lines oil.TextChunk[][] ---@param lines oil.TextChunk[][]
---@param col_width integer[] ---@param col_width integer[]
---@param col_align? oil.ColumnAlign[]
---@return string[] ---@return string[]
---@return any[][] List of highlights {group, lnum, col_start, col_end} ---@return any[][] List of highlights {group, lnum, col_start, col_end}
M.render_table = function(lines, col_width) M.render_table = function(lines, col_width, col_align)
col_align = col_align or {}
local str_lines = {} local str_lines = {}
local highlights = {} local highlights = {}
for _, cols in ipairs(lines) do for _, cols in ipairs(lines) do
@ -294,17 +325,35 @@ M.render_table = function(lines, col_width)
for i, chunk in ipairs(cols) do for i, chunk in ipairs(cols) do
local text, hl local text, hl
if type(chunk) == "table" then if type(chunk) == "table" then
text, hl = unpack(chunk) text = chunk[1]
hl = chunk[2]
else else
text = chunk text = chunk
end end
text = M.rpad(text, col_width[i])
local unpadded_len = text:len()
local padding
text, padding = M.pad_align(text, col_width[i], col_align[i] or "left")
table.insert(pieces, text) table.insert(pieces, text)
local col_end = col + text:len() + 1
if hl then if hl then
table.insert(highlights, { hl, #str_lines, col, col_end }) if type(hl) == "table" then
-- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[]
-- Notice that col_start and col_end are relative position inside
-- that col, so we need to add the offset to them
for _, sub_hl in ipairs(hl) do
table.insert(highlights, {
sub_hl[1],
#str_lines,
col + padding + sub_hl[2],
col + padding + sub_hl[3],
})
end
else
table.insert(highlights, { hl, #str_lines, col + padding, col + padding + unpadded_len })
end
end end
col = col_end col = col + text:len() + 1
end end
table.insert(str_lines, table.concat(pieces, " ")) table.insert(str_lines, table.concat(pieces, " "))
end end
@ -317,15 +366,27 @@ M.set_highlights = function(bufnr, highlights)
local ns = vim.api.nvim_create_namespace("Oil") local ns = vim.api.nvim_create_namespace("Oil")
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
for _, hl in ipairs(highlights) do for _, hl in ipairs(highlights) do
vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl)) 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,
})
end end
end end
---@param path string ---@param path string
---@param os_slash? boolean use os filesystem slash instead of posix slash
---@return string ---@return string
M.addslash = function(path) M.addslash = function(path, os_slash)
if not vim.endswith(path, "/") then local slash = "/"
return path .. "/" if os_slash and require("oil.fs").is_windows then
slash = "\\"
end
local endslash = path:match(slash .. "$")
if not endslash then
return path .. slash
else else
return path return path
end end
@ -337,6 +398,26 @@ M.is_floating_win = function(winid)
return vim.api.nvim_win_get_config(winid or 0).relative ~= "" return vim.api.nvim_win_get_config(winid or 0).relative ~= ""
end 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 = {} local winid_map = {}
M.add_title_to_win = function(winid, opts) M.add_title_to_win = function(winid, opts)
opts = opts or {} opts = opts or {}
@ -344,21 +425,10 @@ M.add_title_to_win = function(winid, opts)
if not vim.api.nvim_win_is_valid(winid) then if not vim.api.nvim_win_is_valid(winid) then
return return
end 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 -- HACK to force the parent window to position itself
-- See https://github.com/neovim/neovim/issues/13403 -- See https://github.com/neovim/neovim/issues/13403
vim.cmd.redraw() vim.cmd.redraw()
local title = get_title() local title = M.get_title(winid)
local width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(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 title_winid = winid_map[winid]
local bufnr local bufnr
@ -406,7 +476,7 @@ M.add_title_to_win = function(winid, opts)
if vim.api.nvim_win_get_buf(winid) ~= winbuf then if vim.api.nvim_win_get_buf(winid) ~= winbuf then
return return
end end
local new_title = get_title() local new_title = M.get_title(winid)
local new_width = local new_width =
math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(new_title)) 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 .. " " }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. new_title .. " " })
@ -453,24 +523,22 @@ end
---@param action oil.Action ---@param action oil.Action
---@return oil.Adapter ---@return oil.Adapter
---@return nil|oil.CrossAdapterAction
M.get_adapter_for_action = function(action) M.get_adapter_for_action = function(action)
local adapter = config.get_adapter_by_scheme(action.url or action.src_url) local adapter = assert(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 if action.dest_url then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if adapter ~= dest_adapter then if adapter ~= dest_adapter then
if if
adapter.supported_adapters_for_copy adapter.supported_cross_adapter_actions
and adapter.supported_adapters_for_copy[dest_adapter.name] and adapter.supported_cross_adapter_actions[dest_adapter.name]
then then
return adapter return adapter, adapter.supported_cross_adapter_actions[dest_adapter.name]
elseif elseif
dest_adapter.supported_adapters_for_copy dest_adapter.supported_cross_adapter_actions
and dest_adapter.supported_adapters_for_copy[adapter.name] and dest_adapter.supported_cross_adapter_actions[adapter.name]
then then
return dest_adapter return dest_adapter, dest_adapter.supported_cross_adapter_actions[adapter.name]
else else
error( error(
string.format( string.format(
@ -578,11 +646,7 @@ M.render_text = function(bufnr, text, opts)
pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines) pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
vim.bo[bufnr].modifiable = false vim.bo[bufnr].modifiable = false
vim.bo[bufnr].modified = false vim.bo[bufnr].modified = false
local ns = vim.api.nvim_create_namespace("Oil") M.set_highlights(bufnr, highlights)
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 end
---Run a function in the context of a full-editor window ---Run a function in the context of a full-editor window
@ -610,8 +674,12 @@ end
---@param bufnr integer ---@param bufnr integer
---@return boolean ---@return boolean
M.is_oil_bufnr = function(bufnr) M.is_oil_bufnr = function(bufnr)
if vim.bo[bufnr].filetype == "oil" then local filetype = vim.bo[bufnr].filetype
if filetype == "oil" then
return true return true
elseif filetype ~= "" then
-- If the filetype is set and is NOT "oil", then it's not an oil buffer
return false
end end
local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr)) local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr))
return config.adapters[scheme] or config.adapter_aliases[scheme] return config.adapters[scheme] or config.adapter_aliases[scheme]
@ -634,15 +702,36 @@ M.hack_around_termopen_autocmd = function(prev_mode)
end, 10) end, 10)
end end
---@param opts? {include_not_owned?: boolean}
---@return nil|integer ---@return nil|integer
M.get_preview_win = function() M.get_preview_win = function(opts)
opts = opts or {}
for _, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do 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 then 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
return winid return winid
end end
end end
end end
---@return fun() restore Function that restores the cursor
M.hide_cursor = function()
vim.api.nvim_set_hl(0, "OilPreviewCursor", { nocombine = true, blend = 100 })
local original_guicursor = vim.go.guicursor
vim.go.guicursor = "a:OilPreviewCursor/OilPreviewCursor"
return function()
-- HACK: see https://github.com/neovim/neovim/issues/21018
vim.go.guicursor = "a:"
vim.cmd.redrawstatus()
vim.go.guicursor = original_guicursor
end
end
---@param bufnr integer ---@param bufnr integer
---@param preferred_win nil|integer ---@param preferred_win nil|integer
---@return nil|integer ---@return nil|integer
@ -696,4 +785,254 @@ M.adapter_list_all = function(adapter, url, opts, callback)
end) end)
end 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}
M.send_to_quickfix = function(opts)
if type(opts) ~= "table" then
opts = {}
end
local oil = require("oil")
local dir = oil.get_current_dir()
if type(dir) ~= "string" then
return
end
local range = M.get_visual_range()
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
local qf_entry = {
filename = dir .. entry.name,
lnum = 1,
col = 1,
text = entry.name,
}
table.insert(qf_entries, qf_entry)
end
end
if #qf_entries == 0 then
vim.notify("[oil] No entries found to send to quickfix", vim.log.levels.WARN)
return
end
vim.api.nvim_exec_autocmds("QuickFixCmdPre", {})
local qf_title = "oil files"
local action = opts.action == "a" and "a" or "r"
if opts.target == "loclist" then
vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries })
vim.cmd.lopen()
else
vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries })
vim.cmd.copen()
end
vim.api.nvim_exec_autocmds("QuickFixCmdPost", {})
end
---@return boolean
M.is_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode
return mode:match("^[vV]") ~= nil
end
---Get the current visual selection range. If not in visual mode, return nil.
---@return {start_lnum: integer, end_lnum: integer}?
M.get_visual_range = function()
if not M.is_visual_mode() then
return
end
-- This is the best way to get the visual selection at the moment
-- https://github.com/neovim/neovim/pull/13896
local _, start_lnum, _, _ = unpack(vim.fn.getpos("v"))
local _, end_lnum, _, _, _ = unpack(vim.fn.getcurpos())
if start_lnum > end_lnum then
start_lnum, end_lnum = end_lnum, start_lnum
end
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)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
if vim.b[bufnr].oil_ready then
callback()
else
vim.api.nvim_create_autocmd("User", {
pattern = "OilEnter",
callback = function(args)
if args.data.buf == bufnr then
vim.api.nvim_buf_call(bufnr, callback)
return true
end
end,
})
end
end
---@param entry oil.Entry
---@return boolean
M.is_directory = function(entry)
local is_directory = entry.type == "directory"
or (
entry.type == "link"
and entry.meta
and entry.meta.link_stat
and entry.meta.link_stat.type == "directory"
)
return is_directory == true
end
---Get the :edit path for an entry
---@param bufnr integer The oil buffer that contains the entry
---@param entry oil.Entry
---@param callback fun(normalized_url: string)
M.get_edit_path = function(bufnr, entry, callback)
local pathutil = require("oil.pathutil")
local bufname = vim.api.nvim_buf_get_name(bufnr)
local scheme, dir = M.parse_url(bufname)
local adapter = M.get_adapter(bufnr, true)
assert(scheme and dir and adapter)
local url = scheme .. dir .. entry.name
if M.is_directory(entry) then
url = url .. "/"
end
if entry.name == ".." then
callback(scheme .. pathutil.parent(dir))
elseif adapter.get_entry_path then
adapter.get_entry_path(url, entry, callback)
else
adapter.normalize_url(url, callback)
end
end
--- Check for an icon provider and return a common icon provider API
---@return (oil.IconProvider)?
M.get_icon_provider = function()
-- prefer mini.icons
local _, mini_icons = pcall(require, "mini.icons")
---@diagnostic disable-next-line: undefined-field
if _G.MiniIcons then -- `_G.MiniIcons` is a better check to see if the module is setup
return function(type, name)
return mini_icons.get(type == "directory" and "directory" or "file", name)
end
end
-- fallback to `nvim-web-devicons`
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
if has_devicons then
return function(type, name, conf)
if type == "directory" then
return conf and conf.directory or "", "OilDirIcon"
else
local icon, hl = devicons.get_icon(name)
icon = icon or (conf and conf.default_file or "")
return icon, hl
end
end
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 return M

View file

@ -1,7 +1,9 @@
local uv = vim.uv or vim.loop
local cache = require("oil.cache") local cache = require("oil.cache")
local columns = require("oil.columns") local columns = require("oil.columns")
local config = require("oil.config") local config = require("oil.config")
local constants = require("oil.constants") local constants = require("oil.constants")
local fs = require("oil.fs")
local keymap_util = require("oil.keymap_util") local keymap_util = require("oil.keymap_util")
local loading = require("oil.loading") local loading = require("oil.loading")
local util = require("oil.util") local util = require("oil.util")
@ -15,13 +17,18 @@ local FIELD_META = constants.FIELD_META
-- map of path->last entry under cursor -- map of path->last entry under cursor
local last_cursor_entry = {} local last_cursor_entry = {}
---@param entry oil.InternalEntry ---@param name string
---@param bufnr integer ---@param bufnr integer
---@return boolean ---@return boolean display
M.should_display = function(entry, bufnr) ---@return boolean is_hidden Whether the file is classified as a hidden file
local name = entry[FIELD_NAME] M.should_display = function(name, bufnr)
return not config.view_options.is_always_hidden(name, bufnr) if config.view_options.is_always_hidden(name, bufnr) then
and (not config.view_options.is_hidden_file(name, bufnr) or config.view_options.show_hidden) 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
end end
---@param bufname string ---@param bufname string
@ -78,7 +85,7 @@ M.toggle_hidden = function()
end end
end end
---@param is_hidden_file fun(filename: string, bufnr: nil|integer): boolean ---@param is_hidden_file fun(filename: string, bufnr: integer): boolean
M.set_is_hidden_file = function(is_hidden_file) M.set_is_hidden_file = function(is_hidden_file)
local any_modified = are_any_modified() local any_modified = are_any_modified()
if any_modified then if any_modified then
@ -111,7 +118,11 @@ M.set_sort = function(new_sort)
end end
end end
---@class oil.ViewData
---@field fs_event? any uv_fs_event_t
-- List of bufnrs -- List of bufnrs
---@type table<integer, oil.ViewData>
local session = {} local session = {}
---@return integer[] ---@return integer[]
@ -135,7 +146,7 @@ M.unlock_buffers = function()
buffers_locked = false buffers_locked = false
for bufnr in pairs(session) do for bufnr in pairs(session) do
if vim.api.nvim_buf_is_loaded(bufnr) then if vim.api.nvim_buf_is_loaded(bufnr) then
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if adapter then if adapter then
vim.bo[bufnr].modifiable = adapter.is_modifiable(bufnr) vim.bo[bufnr].modifiable = adapter.is_modifiable(bufnr)
end end
@ -143,10 +154,12 @@ M.unlock_buffers = function()
end end
end end
---@param opts table ---@param opts? table
---@param callback? fun(err: nil|string)
---@note ---@note
--- This DISCARDS ALL MODIFICATIONS a user has made to oil buffers --- This DISCARDS ALL MODIFICATIONS a user has made to oil buffers
M.rerender_all_oil_buffers = function(opts) M.rerender_all_oil_buffers = function(opts, callback)
opts = opts or {}
local buffers = M.get_all_buffers() local buffers = M.get_all_buffers()
local hidden_buffers = {} local hidden_buffers = {}
for _, bufnr in ipairs(buffers) do for _, bufnr in ipairs(buffers) do
@ -157,29 +170,40 @@ M.rerender_all_oil_buffers = function(opts)
hidden_buffers[vim.api.nvim_win_get_buf(winid)] = nil hidden_buffers[vim.api.nvim_win_get_buf(winid)] = nil
end end
end end
local cb = util.cb_collect(#buffers, callback or function() end)
for _, bufnr in ipairs(buffers) do for _, bufnr in ipairs(buffers) do
if hidden_buffers[bufnr] then if hidden_buffers[bufnr] then
vim.b[bufnr].oil_dirty = opts vim.b[bufnr].oil_dirty = opts
-- We also need to mark this as nomodified so it doesn't interfere with quitting vim -- We also need to mark this as nomodified so it doesn't interfere with quitting vim
vim.bo[bufnr].modified = false vim.bo[bufnr].modified = false
vim.schedule(cb)
else else
M.render_buffer_async(bufnr, opts) M.render_buffer_async(bufnr, opts, cb)
end end
end end
end end
M.set_win_options = function() M.set_win_options = function()
local winid = vim.api.nvim_get_current_win() 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 for k, v in pairs(config.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
end 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 end
---Get a list of visible oil buffers and a list of hidden oil buffers ---Get a list of visible oil buffers and a list of hidden oil buffers
---@note ---@note
--- If any buffers are modified, return values are nil --- If any buffers are modified, return values are nil
---@return nil|integer[] ---@return nil|integer[] visible
---@return nil|integer[] ---@return nil|integer[] hidden
local function get_visible_hidden_buffers() local function get_visible_hidden_buffers()
local buffers = M.get_all_buffers() local buffers = M.get_all_buffers()
local hidden_buffers = {} local hidden_buffers = {}
@ -203,7 +227,12 @@ end
---Delete unmodified, hidden oil buffers and if none remain, clear the cache ---Delete unmodified, hidden oil buffers and if none remain, clear the cache
M.delete_hidden_buffers = function() M.delete_hidden_buffers = function()
local visible_buffers, hidden_buffers = get_visible_hidden_buffers() local visible_buffers, hidden_buffers = get_visible_hidden_buffers()
if not visible_buffers or not hidden_buffers or not vim.tbl_isempty(visible_buffers) then if
not visible_buffers
or not hidden_buffers
or not vim.tbl_isempty(visible_buffers)
or vim.fn.win_gettype() == "command"
then
return return
end end
for _, bufnr in ipairs(hidden_buffers) do for _, bufnr in ipairs(hidden_buffers) do
@ -228,6 +257,106 @@ local function get_first_mutable_column_col(adapter, ranges)
return min_col return min_col
end 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
return
end
local adapter = util.get_adapter(bufnr, true)
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)
end
end
end
---Redraw original path virtual text for trash buffer
---@param bufnr integer
local function redraw_trash_virtual_text(bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) or not vim.api.nvim_buf_is_loaded(bufnr) then
return
end
local parser = require("oil.mutator.parser")
local adapter = util.get_adapter(bufnr, true)
if not adapter or adapter.name ~= "trash" then
return
end
local _, buf_path = util.parse_url(vim.api.nvim_buf_get_name(bufnr))
local os_path = fs.posix_to_os_path(assert(buf_path))
local ns = vim.api.nvim_create_namespace("OilVtext")
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
local column_defs = columns.get_supported_columns(adapter)
for lnum, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)) do
local result = parser.parse_line(adapter, line, column_defs)
local entry = result and result.entry
if entry then
local meta = entry[FIELD_META]
---@type nil|oil.TrashInfo
local trash_info = meta and meta.trash_info
if trash_info then
vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, 0, {
virt_text = {
{
"" .. fs.shorten_path(trash_info.original_path, os_path),
"OilTrashSourcePath",
},
},
})
end
end
end
end
---@param bufnr integer ---@param bufnr integer
M.initialize = function(bufnr) M.initialize = function(bufnr)
if bufnr == 0 then if bufnr == 0 then
@ -245,18 +374,19 @@ M.initialize = function(bufnr)
vim.bo[bufnr].syntax = "oil" vim.bo[bufnr].syntax = "oil"
vim.bo[bufnr].filetype = "oil" vim.bo[bufnr].filetype = "oil"
vim.b[bufnr].EditorConfig_disable = 1 vim.b[bufnr].EditorConfig_disable = 1
session[bufnr] = true session[bufnr] = session[bufnr] or {}
for k, v in pairs(config.buf_options) do for k, v in pairs(config.buf_options) do
vim.api.nvim_buf_set_option(bufnr, k, v) vim.bo[bufnr][k] = v
end end
M.set_win_options() vim.api.nvim_buf_call(bufnr, M.set_win_options)
vim.api.nvim_create_autocmd("BufHidden", { vim.api.nvim_create_autocmd("BufHidden", {
desc = "Delete oil buffers when no longer in use", desc = "Delete oil buffers when no longer in use",
group = "Oil", group = "Oil",
nested = true, nested = true,
buffer = bufnr, buffer = bufnr,
callback = function() callback = function()
-- First wait a short time (10ms) for the buffer change to settle -- First wait a short time (100ms) for the buffer change to settle
vim.defer_fn(function() vim.defer_fn(function()
local visible_buffers = get_visible_hidden_buffers() local visible_buffers = get_visible_hidden_buffers()
-- Only delete oil buffers if none of them are visible -- Only delete oil buffers if none of them are visible
@ -272,16 +402,20 @@ M.initialize = function(bufnr)
end end
end end
end end
end, 10) end, 100)
end, end,
}) })
vim.api.nvim_create_autocmd("BufDelete", { vim.api.nvim_create_autocmd("BufUnload", {
group = "Oil", group = "Oil",
nested = true, nested = true,
once = true, once = true,
buffer = bufnr, buffer = bufnr,
callback = function() callback = function()
local view_data = session[bufnr]
session[bufnr] = nil session[bufnr] = nil
if view_data and view_data.fs_event then
view_data.fs_event:stop()
end
end, end,
}) })
vim.api.nvim_create_autocmd("BufEnter", { vim.api.nvim_create_autocmd("BufEnter", {
@ -296,62 +430,135 @@ M.initialize = function(bufnr)
end, end,
}) })
local timer local timer
vim.api.nvim_create_autocmd("CursorMoved", { vim.api.nvim_create_autocmd("InsertEnter", {
desc = "Constrain oil cursor position",
group = "Oil",
buffer = 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)
end,
})
vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
desc = "Update oil preview window", desc = "Update oil preview window",
group = "Oil", group = "Oil",
buffer = bufnr, buffer = bufnr,
callback = function() callback = function()
local oil = require("oil") local oil = require("oil")
local parser = require("oil.mutator.parser")
if vim.wo.previewwindow then if vim.wo.previewwindow then
return return
end end
-- Force the cursor to be after the (concealed) ID at the beginning of the line constrain_cursor(bufnr, config.constrain_cursor)
local adapter = util.get_adapter(bufnr)
if adapter then
local cur = vim.api.nvim_win_get_cursor(0)
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 = get_first_mutable_column_col(adapter, result.ranges)
if cur[2] < min_col then
vim.api.nvim_win_set_cursor(0, { cur[1], min_col })
end
end
end
-- Debounce and update the preview window if config.preview_win.update_on_cursor_moved then
if timer then -- Debounce and update the preview window
timer:again() if timer then
return timer:again()
end return
timer = vim.loop.new_timer() end
if not timer then timer = uv.new_timer()
return if not timer then
end return
timer:start(10, 100, function() end
timer:stop() timer:start(10, 100, function()
timer:close() timer:stop()
timer = nil timer:close()
vim.schedule(function() timer = nil
if vim.api.nvim_get_current_buf() ~= bufnr then vim.schedule(function()
return if vim.api.nvim_get_current_buf() ~= bufnr then
end return
local entry = oil.get_cursor_entry() end
if entry then local entry = oil.get_cursor_entry()
local winid = util.get_preview_win() -- Don't update in visual mode. Visual mode implies editing not browsing,
if winid then -- and updating the preview can cause flicker and stutter.
if entry.id ~= vim.w[winid].oil_entry_id then if entry and not util.is_visual_mode() then
oil.select({ preview = true }) local winid = util.get_preview_win()
if winid then
if entry.id ~= vim.w[winid].oil_entry_id then
oil.open_preview()
end
end end
end end
end end)
end) end)
end) end
end, end,
}) })
local adapter = util.get_adapter(bufnr, true)
-- Set up a watcher that will refresh the directory
if
adapter
and adapter.name == "files"
and config.watch_for_changes
and not session[bufnr].fs_event
then
local fs_event = assert(uv.new_fs_event())
local bufname = vim.api.nvim_buf_get_name(bufnr)
local _, dir = util.parse_url(bufname)
fs_event:start(
assert(dir),
{},
vim.schedule_wrap(function(err, filename, events)
if not vim.api.nvim_buf_is_valid(bufnr) then
local sess = session[bufnr]
if sess then
sess.fs_event = nil
end
fs_event:stop()
return
end
local mutator = require("oil.mutator")
if err or vim.bo[bufnr].modified or vim.b[bufnr].oil_dirty or mutator.is_mutating() then
return
end
-- If the buffer is currently visible, rerender
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
M.render_buffer_async(bufnr)
return
end
end
-- If it is not currently visible, mark it as dirty
vim.b[bufnr].oil_dirty = {}
end)
)
session[bufnr].fs_event = fs_event
end
-- Watch for TextChanged and update the trash original path extmarks
if adapter and adapter.name == "trash" then
local debounce_timer = assert(uv.new_timer())
local pending = false
vim.api.nvim_create_autocmd("TextChanged", {
desc = "Update oil virtual text of original path",
buffer = bufnr,
callback = function()
-- Respond immediately to prevent flickering, the set the timer for a "cooldown period"
-- If this is called again during the cooldown window, we will rerender after cooldown.
if debounce_timer:is_active() then
pending = true
else
redraw_trash_virtual_text(bufnr)
end
debounce_timer:start(
50,
0,
vim.schedule_wrap(function()
if pending then
pending = false
redraw_trash_virtual_text(bufnr)
end
end)
)
end,
})
end
M.render_buffer_async(bufnr, {}, function(err) M.render_buffer_async(bufnr, {}, function(err)
if err then if err then
vim.notify( vim.notify(
@ -359,6 +566,7 @@ M.initialize = function(bufnr)
vim.log.levels.ERROR vim.log.levels.ERROR
) )
else else
vim.b[bufnr].oil_ready = true
vim.api.nvim_exec_autocmds( vim.api.nvim_exec_autocmds(
"User", "User",
{ pattern = "OilEnter", modeline = false, data = { buf = bufnr } } { pattern = "OilEnter", modeline = false, data = { buf = bufnr } }
@ -369,10 +577,18 @@ M.initialize = function(bufnr)
end end
---@param adapter oil.Adapter ---@param adapter oil.Adapter
---@param num_entries integer
---@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean ---@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean
local function get_sort_function(adapter) local function get_sort_function(adapter, num_entries)
local idx_funs = {} local idx_funs = {}
for _, sort_pair in ipairs(config.view_options.sort) do local sort_config = config.view_options.sort
-- If empty, default to type + name sorting
if vim.tbl_isempty(sort_config) then
sort_config = { { "type", "asc" }, { "name", "asc" } }
end
for _, sort_pair in ipairs(sort_config) do
local col_name, order = unpack(sort_pair) local col_name, order = unpack(sort_pair)
if order ~= "asc" and order ~= "desc" then if order ~= "asc" and order ~= "desc" then
vim.notify_once( vim.notify_once(
@ -385,7 +601,9 @@ local function get_sort_function(adapter)
) )
end end
local col = columns.get_column(adapter, col_name) local col = columns.get_column(adapter, col_name)
if col and col.get_sort_value then 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
table.insert(idx_funs, { col.get_sort_value, order }) table.insert(idx_funs, { col.get_sort_value, order })
else else
vim.notify_once( vim.notify_once(
@ -396,7 +614,7 @@ local function get_sort_function(adapter)
end end
return function(a, b) return function(a, b)
for _, sort_fn in ipairs(idx_funs) do for _, sort_fn in ipairs(idx_funs) do
local get_sort_value, order = unpack(sort_fn) local get_sort_value, order = sort_fn[1], sort_fn[2]
local a_val = get_sort_value(a) local a_val = get_sort_value(a)
local b_val = get_sort_value(b) local b_val = get_sort_value(b)
if a_val ~= b_val then if a_val ~= b_val then
@ -429,14 +647,17 @@ local function render_buffer(bufnr, opts)
jump_first = false, jump_first = false,
}) })
local scheme = util.parse_url(bufname) local scheme = util.parse_url(bufname)
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if not scheme or not adapter then if not scheme or not adapter then
return false return false
end end
local entries = cache.list_url(bufname) local entries = cache.list_url(bufname)
local entry_list = vim.tbl_values(entries) local entry_list = vim.tbl_values(entries)
table.sort(entry_list, get_sort_function(adapter)) -- 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
local jump_idx local jump_idx
if opts.jump_first then if opts.jump_first then
@ -447,50 +668,63 @@ local function render_buffer(bufnr, opts)
local column_defs = columns.get_supported_columns(scheme) local column_defs = columns.get_supported_columns(scheme)
local line_table = {} local line_table = {}
local col_width = {} local col_width = {}
for i in ipairs(column_defs) do local col_align = {}
for i, col_def in ipairs(column_defs) do
col_width[i + 1] = 1 col_width[i + 1] = 1
local _, conf = util.split_config(col_def)
col_align[i + 1] = conf and conf.align or "left"
end end
for _, entry in ipairs(entry_list) do
if not M.should_display(entry, bufnr) then if M.should_display("..", bufnr) then
goto continue local cols =
end M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter, true, bufnr)
local cols = M.format_entry_cols(entry, column_defs, col_width, adapter)
table.insert(line_table, cols) 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
::continue::
end end
local lines, highlights = util.render_table(line_table, col_width) 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)
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
end
end
end
local lines, highlights = util.render_table(line_table, col_width, col_align)
vim.bo[bufnr].modifiable = true vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
vim.bo[bufnr].modifiable = false vim.bo[bufnr].modifiable = false
vim.bo[bufnr].modified = false vim.bo[bufnr].modified = false
util.set_highlights(bufnr, highlights) util.set_highlights(bufnr, highlights)
if opts.jump then if opts.jump then
-- TODO why is the schedule necessary? -- TODO why is the schedule necessary?
vim.schedule(function() vim.schedule(function()
for _, winid in ipairs(vim.api.nvim_list_wins()) do 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 vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then
-- If we're not jumping to a specific lnum, use the current lnum so we can adjust the col if jump_idx then
local lnum = jump_idx or vim.api.nvim_win_get_cursor(winid)[1] local lnum = jump_idx
local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1] local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
local id_str = line:match("^/(%d+)") local id_str = line:match("^/(%d+)")
local id = tonumber(id_str) local id = tonumber(id_str)
if id then if id then
local entry = cache.get_entry_by_id(id) local entry = cache.get_entry_by_id(id)
if entry then if entry then
local name = entry[FIELD_NAME] local name = entry[FIELD_NAME]
local col = line:find(name, 1, true) or (id_str:len() + 1) local col = line:find(name, 1, true) or (id_str:len() + 1)
vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 }) vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 })
return
end
end end
end end
constrain_cursor(bufnr, "name")
end end
end end
end) end)
@ -498,14 +732,48 @@ local function render_buffer(bufnr, opts)
return seek_after_render_found return seek_after_render_found
end 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 ---@private
---@param entry oil.InternalEntry ---@param entry oil.InternalEntry
---@param column_defs table[] ---@param column_defs table[]
---@param col_width integer[] ---@param col_width integer[]
---@param adapter oil.Adapter ---@param adapter oil.Adapter
---@param is_hidden boolean
---@param bufnr integer
---@return oil.TextChunk[] ---@return oil.TextChunk[]
M.format_entry_cols = function(entry, column_defs, col_width, adapter) M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden, bufnr)
local name = entry[FIELD_NAME] 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 -- First put the unique ID
local cols = {} local cols = {}
local id_key = cache.format_id(entry[FIELD_ID]) local id_key = cache.format_id(entry[FIELD_ID])
@ -513,7 +781,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
table.insert(cols, id_key) table.insert(cols, id_key)
-- Then add all the configured columns -- Then add all the configured columns
for i, column in ipairs(column_defs) do for i, column in ipairs(column_defs) do
local chunk = columns.render_col(adapter, column, entry) local chunk = columns.render_col(adapter, column, entry, bufnr)
local text = type(chunk) == "table" and chunk[1] or chunk local text = type(chunk) == "table" and chunk[1] or chunk
---@cast text string ---@cast text string
col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text)) col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text))
@ -521,33 +789,59 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
end end
-- Always add the entry name at the end -- Always add the entry name at the end
local entry_type = entry[FIELD_TYPE] local entry_type = entry[FIELD_TYPE]
if entry_type == "directory" then
table.insert(cols, { name .. "/", "OilDir" }) local get_custom_hl = config.view_options.highlight_filename
elseif entry_type == "socket" then local link_name, link_name_hl, link_target, link_target_hl
table.insert(cols, { name, "OilSocket" }) if get_custom_hl then
elseif entry_type == "link" then local external_entry = util.export_entry(entry)
local meta = entry[FIELD_META]
local link_text if entry_type == "link" then
if meta then link_name, link_target = get_link_text(name, meta)
if meta.link_stat and meta.link_stat.type == "directory" then local is_orphan = not (meta and meta.link_stat)
name = name .. "/" 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 end
if meta.link then -- intentional fallthrough
link_text = "->" .. " " .. meta.link else
if meta.link_stat and meta.link_stat.type == "directory" then local hl = get_custom_hl(external_entry, is_hidden, false, false, bufnr)
link_text = util.addslash(link_text) if hl then
-- Add the trailing / if this is a directory, this is important
if entry_type == "directory" then
name = name .. "/"
end end
table.insert(cols, { name, hl })
return cols
end end
end end
end
table.insert(cols, { name, "OilLink" }) if entry_type == "directory" then
if link_text then table.insert(cols, { name .. "/", "OilDir" .. hl_suffix })
table.insert(cols, { link_text, "Comment" }) elseif entry_type == "socket" then
table.insert(cols, { name, "OilSocket" .. hl_suffix })
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
end
table.insert(cols, { link_target, link_target_hl })
end end
else else
table.insert(cols, { name, "OilFile" }) table.insert(cols, { name, "OilFile" .. hl_suffix })
end end
return cols return cols
end end
@ -566,32 +860,47 @@ local function get_used_columns()
return cols return cols
end end
---@type table<integer, fun(message: string)[]>
local pending_renders = {}
---@param bufnr integer ---@param bufnr integer
---@param opts nil|table ---@param opts nil|table
--- preserve_undo nil|boolean
--- refetch nil|boolean Defaults to true --- refetch nil|boolean Defaults to true
---@param callback nil|fun(err: nil|string) ---@param callback nil|fun(err: nil|string)
M.render_buffer_async = function(bufnr, opts, callback) M.render_buffer_async = function(bufnr, opts, callback)
opts = vim.tbl_deep_extend("keep", opts or {}, { opts = vim.tbl_deep_extend("keep", opts or {}, {
preserve_undo = false,
refetch = true, refetch = true,
}) })
if bufnr == 0 then if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf() bufnr = vim.api.nvim_get_current_buf()
end end
local bufname = vim.api.nvim_buf_get_name(bufnr)
local scheme, dir = util.parse_url(bufname) -- If we're already rendering, queue up another rerender after it's complete
local preserve_undo = opts.preserve_undo and config.adapters[scheme] == "files" if vim.b[bufnr].oil_rendering then
if not preserve_undo then if not pending_renders[bufnr] then
-- Undo should not return to a blank buffer pending_renders[bufnr] = { callback }
-- Method taken from :h clear-undo elseif callback then
vim.bo[bufnr].undolevels = -1 table.insert(pending_renders[bufnr], callback)
end
local handle_error = vim.schedule_wrap(function(message)
if not preserve_undo then
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" })
end end
return
end
local bufname = vim.api.nvim_buf_get_name(bufnr)
vim.b[bufnr].oil_rendering = true
local _, dir = util.parse_url(bufname)
-- Undo should not return to a blank buffer
-- Method taken from :h clear-undo
vim.bo[bufnr].undolevels = -1
local handle_error = vim.schedule_wrap(function(message)
vim.b[bufnr].oil_rendering = false
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" })
util.render_text(bufnr, { "Error: " .. message }) util.render_text(bufnr, { "Error: " .. message })
if pending_renders[bufnr] then
for _, cb in ipairs(pending_renders[bufnr]) do
cb(message)
end
pending_renders[bufnr] = nil
end
if callback then if callback then
callback(message) callback(message)
else else
@ -602,30 +911,43 @@ M.render_buffer_async = function(bufnr, opts, callback)
handle_error(string.format("Could not parse oil url '%s'", bufname)) handle_error(string.format("Could not parse oil url '%s'", bufname))
return return
end end
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if not adapter then if not adapter then
handle_error(string.format("[oil] no adapter for buffer '%s'", bufname)) handle_error(string.format("[oil] no adapter for buffer '%s'", bufname))
return return
end end
local start_ms = vim.loop.hrtime() / 1e6 local start_ms = uv.hrtime() / 1e6
local seek_after_render_found = false local seek_after_render_found = false
local first = true local first = true
vim.bo[bufnr].modifiable = false vim.bo[bufnr].modifiable = false
vim.bo[bufnr].modified = false
loading.set_loading(bufnr, true) loading.set_loading(bufnr, true)
local finish = vim.schedule_wrap(function() local finish = vim.schedule_wrap(function()
if not vim.api.nvim_buf_is_valid(bufnr) then if not vim.api.nvim_buf_is_valid(bufnr) then
return return
end end
vim.b[bufnr].oil_rendering = false
loading.set_loading(bufnr, false) loading.set_loading(bufnr, false)
render_buffer(bufnr, { jump = true }) render_buffer(bufnr, { jump = true })
if not preserve_undo then M.set_last_cursor(bufname, nil)
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" })
end
vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr) vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr)
if callback then if callback then
callback() callback()
end end
-- If there were any concurrent calls to render this buffer, process them now
if pending_renders[bufnr] then
local all_cbs = pending_renders[bufnr]
pending_renders[bufnr] = nil
local new_cb = function(...)
for _, cb in ipairs(all_cbs) do
cb(...)
end
end
M.render_buffer_async(bufnr, {}, new_cb)
end
end) end)
if not opts.refetch then if not opts.refetch then
finish() finish()
@ -633,6 +955,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
end end
cache.begin_update_url(bufname) cache.begin_update_url(bufname)
local num_iterations = 0
adapter.list(bufname, get_used_columns(), function(err, entries, fetch_more) adapter.list(bufname, get_used_columns(), function(err, entries, fetch_more)
loading.set_loading(bufnr, false) loading.set_loading(bufnr, false)
if err then if err then
@ -646,14 +969,16 @@ M.render_buffer_async = function(bufnr, opts, callback)
end end
end end
if fetch_more then if fetch_more then
local now = vim.loop.hrtime() / 1e6 local now = uv.hrtime() / 1e6
local delta = now - start_ms local delta = now - start_ms
-- If we've been chugging for more than 40ms, go ahead and render what we have -- If we've been chugging for more than 40ms, go ahead and render what we have
if delta > 40 then if (delta > 25 and num_iterations < 1) or delta > 500 then
num_iterations = num_iterations + 1
start_ms = now start_ms = now
vim.schedule(function() vim.schedule(function()
seek_after_render_found = seek_after_render_found =
render_buffer(bufnr, { jump = not seek_after_render_found, jump_first = first }) render_buffer(bufnr, { jump = not seek_after_render_found, jump_first = first })
start_ms = uv.hrtime() / 1e6
end) end)
end end
first = false first = false

63
perf/bootstrap.lua Normal file
View file

@ -0,0 +1,63 @@
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

View file

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
mkdir -p ".testenv/config/nvim" mkdir -p ".testenv/config/nvim"

View file

@ -2,20 +2,21 @@ import os
import os.path import os.path
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List from typing import Any, Dict, List
from nvim_doc_tools import ( from nvim_doc_tools import (
LuaParam, LuaParam,
LuaTypes,
Vimdoc, Vimdoc,
VimdocSection, VimdocSection,
generate_md_toc, generate_md_toc,
indent, indent,
leftright, leftright,
parse_functions, parse_directory,
read_nvim_json, read_nvim_json,
read_section, read_section,
render_md_api, render_md_api2,
render_vimdoc_api, render_vimdoc_api2,
replace_section, replace_section,
wrap, wrap,
) )
@ -37,8 +38,9 @@ def add_md_link_path(path: str, lines: List[str]) -> List[str]:
def update_md_api(): def update_md_api():
api_doc = os.path.join(DOC, "api.md") api_doc = os.path.join(DOC, "api.md")
funcs = parse_functions(os.path.join(ROOT, "lua", "oil", "init.lua")) types = parse_directory(os.path.join(ROOT, "lua"))
lines = ["\n"] + render_md_api(funcs, 2) + ["\n"] funcs = types.files["oil/init.lua"].functions
lines = ["\n"] + render_md_api2(funcs, types, 2) + ["\n"]
replace_section( replace_section(
api_doc, api_doc,
r"^<!-- API -->$", r"^<!-- API -->$",
@ -61,10 +63,25 @@ def update_md_api():
) )
def update_readme_toc(): def update_readme():
toc = ["\n"] + generate_md_toc(README, max_level=1) + ["\n"] def get_toc(filename: str) -> List[str]:
subtoc = generate_md_toc(os.path.join(DOC, filename))
return add_md_link_path("doc/" + filename, subtoc)
recipes_toc = get_toc("recipes.md")
replace_section( replace_section(
README, README,
r"^## Recipes$",
r"^#",
["\n"] + recipes_toc + ["\n"],
)
def update_md_toc(filename: str, max_level: int = 99):
toc = ["\n"] + generate_md_toc(filename, max_level) + ["\n"]
replace_section(
filename,
r"^<!-- TOC -->$", r"^<!-- TOC -->$",
r"^<!-- /TOC -->$", r"^<!-- /TOC -->$",
toc, toc,
@ -93,11 +110,16 @@ class ColumnDef:
params: List["LuaParam"] = field(default_factory=list) params: List["LuaParam"] = field(default_factory=list)
HL = [ UNIVERSAL = [
LuaParam( LuaParam(
"highlight", "highlight",
"string|fun(value: string): string", "string|fun(value: string): string",
"Highlight group, or function that returns a highlight group", "Highlight group, or function that returns a highlight group",
),
LuaParam(
"align",
'"left"|"center"|"right"',
"Text alignment within the column",
) )
] ]
TIME = [ TIME = [
@ -110,7 +132,7 @@ COL_DEFS = [
False, False,
True, True,
"The type of the entry (file, directory, link, etc)", "The type of the entry (file, directory, link, etc)",
HL UNIVERSAL
+ [LuaParam("icons", "table<string, string>", "Mapping of entry type to icon")], + [LuaParam("icons", "table<string, string>", "Mapping of entry type to icon")],
), ),
ColumnDef( ColumnDef(
@ -119,30 +141,52 @@ COL_DEFS = [
False, False,
False, False,
"An icon for the entry's type (requires nvim-web-devicons)", "An icon for the entry's type (requires nvim-web-devicons)",
HL UNIVERSAL
+ [ + [
LuaParam("default_file", "string", "Fallback icon for files when nvim-web-devicons returns nil"), LuaParam(
"default_file",
"string",
"Fallback icon for files when nvim-web-devicons returns nil",
),
LuaParam("directory", "string", "Icon for directories"), LuaParam("directory", "string", "Icon for directories"),
LuaParam("add_padding", "boolean", "Set to false to remove the extra whitespace after the icon"), LuaParam(
"add_padding",
"boolean",
"Set to false to remove the extra whitespace after the icon",
),
], ],
), ),
ColumnDef("size", "files, ssh", False, True, "The size of the file", HL + []), ColumnDef("size", "files, ssh, s3", False, True, "The size of the file", UNIVERSAL + []),
ColumnDef( ColumnDef(
"permissions", "files, ssh", True, False, "Access permissions of the file", HL + [] "permissions",
"files, ssh",
True,
False,
"Access permissions of the file",
UNIVERSAL + [],
), ),
ColumnDef("ctime", "files", False, True, "Change timestamp of the file", HL + TIME + []),
ColumnDef( ColumnDef(
"mtime", "files", False, True, "Last modified time of the file", HL + TIME + [] "ctime", "files", False, True, "Change timestamp of the file", UNIVERSAL + TIME + []
), ),
ColumnDef("atime", "files", False, True, "Last access time of the file", HL + TIME + []),
ColumnDef( ColumnDef(
"birthtime", "files", False, True, "The time the file was created", HL + TIME + [] "mtime", "files", False, True, "Last modified time of the file", UNIVERSAL + TIME + []
),
ColumnDef(
"atime", "files", False, True, "Last access time of the file", UNIVERSAL + TIME + []
),
ColumnDef(
"birthtime",
"files, s3",
False,
True,
"The time the file was created",
UNIVERSAL + TIME + [],
), ),
] ]
def get_options_vimdoc() -> "VimdocSection": def get_options_vimdoc() -> "VimdocSection":
section = VimdocSection("options", "oil-options") section = VimdocSection("config", "oil-config")
config_file = os.path.join(ROOT, "lua", "oil", "config.lua") config_file = os.path.join(ROOT, "lua", "oil", "config.lua")
opt_lines = read_section(config_file, r"^local default_config =", r"^}$") opt_lines = read_section(config_file, r"^local default_config =", r"^}$")
lines = ["\n", ">lua\n", ' require("oil").setup({\n'] lines = ["\n", ">lua\n", ' require("oil").setup({\n']
@ -152,6 +196,43 @@ def get_options_vimdoc() -> "VimdocSection":
return section return section
def get_options_detail_vimdoc() -> "VimdocSection":
section = VimdocSection("options", "oil-options")
section.body.append(
"""
skip_confirm_for_simple_edits *oil.skip_confirm_for_simple_edits*
type: `boolean` default: `false`
Before performing filesystem operations, Oil displays a confirmation popup to ensure
that all operations are intentional. When this option is `true`, the popup will be
skipped if the operations:
* contain no deletes
* contain no cross-adapter moves or copies (e.g. from local to ssh)
* contain at most one copy or move
* contain at most five creates
prompt_save_on_select_new_entry *oil.prompt_save_on_select_new_entry*
type: `boolean` default: `true`
There are two cases where this option is relevant:
1. You copy a file to a new location, then you select it and make edits before
saving.
2. You copy a directory to a new location, then you enter the directory and make
changes before saving.
In case 1, when you edit the file you are actually editing the original file because
oil has not yet moved/copied it to its new location. This means that the original
file will, perhaps unexpectedly, also be changed by any edits you make.
Case 2 is similar; when you edit the directory you are again actually editing the
original location of the directory. If you add new files, those files will be
created in both the original location and the copied directory.
When this option is `true`, Oil will prompt you to save before entering a file or
directory that is pending within oil, but does not exist on disk.
"""
)
return section
def get_highlights_vimdoc() -> "VimdocSection": def get_highlights_vimdoc() -> "VimdocSection":
section = VimdocSection("Highlights", "oil-highlights", ["\n"]) section = VimdocSection("Highlights", "oil-highlights", ["\n"])
highlights = read_nvim_json('require("oil")._get_highlights()') highlights = read_nvim_json('require("oil")._get_highlights()')
@ -166,21 +247,76 @@ def get_highlights_vimdoc() -> "VimdocSection":
return section return section
def load_params(params: Dict[str, Any]) -> List[LuaParam]:
ret = []
for name, data in sorted(params.items()):
ret.append(LuaParam(name, data["type"], data["desc"]))
return ret
def get_actions_vimdoc() -> "VimdocSection": def get_actions_vimdoc() -> "VimdocSection":
section = VimdocSection("Actions", "oil-actions", ["\n"]) section = VimdocSection("Actions", "oil-actions", ["\n"])
section.body.append(
"""The `keymaps` option in `oil.setup` allow you to create mappings using all the same parameters as |vim.keymap.set|.
>lua
keymaps = {
-- Mappings can be a string
["~"] = "<cmd>edit $HOME<CR>",
-- Mappings can be a function
["gd"] = function()
require("oil").set_columns({ "icon", "permissions", "size", "mtime" })
end,
-- You can pass additional opts to vim.keymap.set by using
-- a table with the mapping as the first element.
["<leader>ff"] = {
function()
require("telescope.builtin").find_files({
cwd = require("oil").get_current_dir()
})
end,
mode = "n",
nowait = true,
desc = "Find files in the current directory"
},
-- Mappings that are a string starting with "actions." will be
-- one of the built-in actions, documented below.
["`"] = "actions.tcd",
-- Some actions have parameters. These are passed in via the `opts` key.
["<leader>:"] = {
"actions.open_cmdline",
opts = {
shorten_path = true,
modify = ":h",
},
desc = "Open the command line with the current directory as an argument",
},
}
"""
)
section.body.append("\n")
section.body.extend( section.body.extend(
wrap( wrap(
"These are actions that can be used in the `keymaps` section of config options." """Below are the actions that can be used in the `keymaps` section of config options. You can refer to them as strings (e.g. "actions.<action_name>") or you can use the functions directly with `require("oil.actions").action_name.callback()`"""
) )
) )
section.body.append("\n") section.body.append("\n")
actions = read_nvim_json('require("oil.actions")._get_actions()') actions = read_nvim_json('require("oil.actions")._get_actions()')
actions.sort(key=lambda a: a["name"]) actions.sort(key=lambda a: a["name"])
for action in actions: for action in actions:
if action.get("deprecated"):
continue
name = action["name"] name = action["name"]
desc = action["desc"] desc = action["desc"]
section.body.append(leftright(name, f"*actions.{name}*")) section.body.append(leftright(name, f"*actions.{name}*"))
section.body.extend(wrap(desc, 4)) section.body.extend(wrap(desc, 4))
params = action.get("parameters")
if params:
section.body.append("\n")
section.body.append(" Parameters:\n")
section.body.extend(
format_vimdoc_params(load_params(params), LuaTypes(), 6)
)
section.body.append("\n") section.body.append("\n")
return section return section
@ -205,21 +341,55 @@ def get_columns_vimdoc() -> "VimdocSection":
section.body.extend(wrap(col.summary, 4)) section.body.extend(wrap(col.summary, 4))
section.body.append("\n") section.body.append("\n")
section.body.append(" Parameters:\n") section.body.append(" Parameters:\n")
section.body.extend(format_vimdoc_params(col.params, 6)) section.body.extend(format_vimdoc_params(col.params, LuaTypes(), 6))
section.body.append("\n") section.body.append("\n")
return section return section
def get_trash_vimdoc() -> "VimdocSection":
section = VimdocSection("Trash", "oil-trash", [])
section.body.append(
"""
Oil has built-in support for using the system trash. When
`delete_to_trash = true`, any deleted files will be sent to the trash instead
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).
Linux:
Oil supports the FreeDesktop trash specification.
https://specifications.freedesktop.org/trash-spec/1.0/
All features should work.
Mac:
Oil has limited support for MacOS due to the proprietary nature of the
implementation. The trash bin can only be viewed as a single dir
(instead of being able to see files that were trashed from a directory).
Windows:
Oil supports the Windows Recycle Bin. All features should work.
"""
)
return section
def generate_vimdoc(): def generate_vimdoc():
doc = Vimdoc("oil.txt", "oil") doc = Vimdoc("oil.txt", "oil")
funcs = parse_functions(os.path.join(ROOT, "lua", "oil", "init.lua")) types = parse_directory(os.path.join(ROOT, "lua"))
funcs = types.files["oil/init.lua"].functions
doc.sections.extend( doc.sections.extend(
[ [
get_options_vimdoc(), get_options_vimdoc(),
VimdocSection("API", "oil-api", render_vimdoc_api("oil", funcs)), get_options_detail_vimdoc(),
VimdocSection("API", "oil-api", render_vimdoc_api2("oil", funcs, types)),
get_columns_vimdoc(), get_columns_vimdoc(),
get_actions_vimdoc(), get_actions_vimdoc(),
get_highlights_vimdoc(), get_highlights_vimdoc(),
get_trash_vimdoc(),
] ]
) )
@ -231,5 +401,7 @@ def main() -> None:
"""Update the README""" """Update the README"""
update_config_options() update_config_options()
update_md_api() update_md_api()
update_readme_toc() update_md_toc(README, max_level=1)
update_md_toc(os.path.join(DOC, "recipes.md"))
update_readme()
generate_vimdoc() generate_vimdoc()

4
scripts/requirements.txt Normal file
View file

@ -0,0 +1,4 @@
pyparsing==3.0.9
black
isort
mypy

View file

@ -2,10 +2,14 @@ if exists("b:current_syntax")
finish finish
endif endif
syn match oilCreate /^CREATE / syn match oilCreate /^CREATE\( BUCKET\)\? /
syn match oilMove /^ MOVE / syn match oilMove /^ MOVE /
syn match oilDelete /^DELETE / syn match oilDelete /^DELETE\( BUCKET\)\? /
syn match oilCopy /^ COPY / syn match oilCopy /^ COPY /
syn match oilChange /^CHANGE / syn match oilChange /^CHANGE /
" Trash operations
syn match oilRestore /^RESTORE /
syn match oilPurge /^ PURGE /
syn match oilTrash /^ TRASH /
let b:current_syntax = "oil_preview" let b:current_syntax = "oil_preview"

View file

@ -104,7 +104,7 @@ a.describe("Alternate buffer", function()
return oil.get_cursor_entry() return oil.get_cursor_entry()
end, 10) end, 10)
vim.api.nvim_win_set_cursor(0, { 1, 1 }) vim.api.nvim_win_set_cursor(0, { 1, 1 })
oil.select({ preview = true }) oil.open_preview()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) end)
@ -141,5 +141,17 @@ a.describe("Alternate buffer", function()
oil.close() oil.close()
assert.equals("foo", vim.fn.expand("#")) assert.equals("foo", vim.fn.expand("#"))
end) 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<CR>" }, 10)
oil.select()
test_util.wait_for_autocmd("BufEnter")
assert.equals("LICENSE", vim.fn.expand("%:."))
assert.equals("foo", vim.fn.expand("#"))
end)
end) end)
end) end)

View file

@ -11,8 +11,6 @@ a.describe("files adapter", function()
a.after_each(function() a.after_each(function()
if tmpdir then if tmpdir then
tmpdir:dispose() tmpdir:dispose()
a.util.scheduler()
tmpdir = nil
end end
test_util.reset_editor() test_util.reset_editor()
end) end)
@ -152,10 +150,10 @@ a.describe("files adapter", function()
a.it("Editing a new oil://path/ creates an oil buffer", function() a.it("Editing a new oil://path/ creates an oil buffer", function()
local tmpdir_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "/" local tmpdir_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "/"
vim.cmd.edit({ args = { tmpdir_url } }) vim.cmd.edit({ args = { tmpdir_url } })
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) test_util.wait_oil_ready()
local new_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "newdir" local new_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "newdir"
vim.cmd.edit({ args = { new_url } }) vim.cmd.edit({ args = { new_url } })
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) test_util.wait_oil_ready()
assert.equals("oil", vim.bo.filetype) assert.equals("oil", vim.bo.filetype)
-- The normalization will add a '/' -- The normalization will add a '/'
assert.equals(new_url .. "/", vim.api.nvim_buf_get_name(0)) assert.equals(new_url .. "/", vim.api.nvim_buf_get_name(0))
@ -170,5 +168,6 @@ a.describe("files adapter", function()
test_util.wait_for_autocmd("BufReadPost") test_util.wait_for_autocmd("BufReadPost")
assert.equals("ruby", vim.bo.filetype) 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(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)
end) end)

28
tests/manual_progress.lua Normal file
View file

@ -0,0 +1,28 @@
-- Manual test for minimizing/restoring progress window
local Progress = require("oil.mutator.progress")
local progress = Progress.new()
progress:show({
cancel = function()
progress:close()
end,
})
for i = 1, 10, 1 do
vim.defer_fn(function()
progress:set_action({
type = "create",
url = string.format("oil:///tmp/test_%d.txt", i),
entry_type = "file",
}, i, 10)
end, (i - 1) * 1000)
end
vim.defer_fn(function()
progress:close()
end, 10000)
vim.keymap.set("n", "R", function()
progress:restore()
end, {})

View file

@ -1,4 +1,4 @@
vim.cmd([[set runtimepath+=.]]) vim.opt.runtimepath:append(".")
vim.o.swapfile = false vim.o.swapfile = false
vim.bo.swapfile = false vim.bo.swapfile = false

View file

@ -216,6 +216,26 @@ a.describe("mutator", function()
assert.are.same({ move1, move2 }, ordered_actions) assert.are.same({ move1, move2 }, ordered_actions)
end) end)
it("Handles a delete inside a moved folder", function()
-- delete in directory and move directory
-- DELETE /a/b.txt
-- MOVE /a/ -> /b/
local del = {
type = "delete",
url = "oil-test:///a/b.txt",
entry_type = "file",
}
local move = {
type = "move",
src_url = "oil-test:///a",
dest_url = "oil-test:///b",
entry_type = "directory",
}
local actions = { move, del }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ del, move }, ordered_actions)
end)
it("Detects move directory loops", function() it("Detects move directory loops", function()
local move = { local move = {
type = "move", type = "move",

View file

@ -18,6 +18,7 @@ describe("parser", function()
after_each(function() after_each(function()
test_util.reset_editor() test_util.reset_editor()
end) end)
it("detects new files", function() it("detects new files", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } }) vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
@ -89,7 +90,7 @@ describe("parser", function()
local file = test_adapter.test_set("/foo/a.txt", "file") local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } }) vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local cols = view.format_entry_cols(file, {}, {}, test_adapter) local cols = view.format_entry_cols(file, {}, {}, test_adapter, false)
local lines = util.render_table({ cols }, {}) local lines = util.render_table({ cols }, {})
table.insert(lines, "") table.insert(lines, "")
table.insert(lines, " ") table.insert(lines, " ")
@ -109,6 +110,7 @@ describe("parser", function()
{ {
message = "Malformed ID at start of line", message = "Malformed ID at start of line",
lnum = 0, lnum = 0,
end_lnum = 1,
col = 0, col = 0,
}, },
}, errors) }, errors)
@ -125,6 +127,7 @@ describe("parser", function()
{ {
message = "No filename found", message = "No filename found",
lnum = 0, lnum = 0,
end_lnum = 1,
col = 0, col = 0,
}, },
}, errors) }, errors)
@ -142,6 +145,7 @@ describe("parser", function()
{ {
message = "Duplicate filename", message = "Duplicate filename",
lnum = 1, lnum = 1,
end_lnum = 2,
col = 0, col = 0,
}, },
}, errors) }, errors)
@ -160,6 +164,7 @@ describe("parser", function()
{ {
message = "Duplicate filename", message = "Duplicate filename",
lnum = 1, lnum = 1,
end_lnum = 2,
col = 0, col = 0,
}, },
}, errors) }, errors)

41
tests/preview_spec.lua Normal file
View file

@ -0,0 +1,41 @@
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)

View file

@ -1,5 +1,6 @@
require("plenary.async").tests.add_to_env() require("plenary.async").tests.add_to_env()
local TmpDir = require("tests.tmpdir") local TmpDir = require("tests.tmpdir")
local actions = require("oil.actions")
local oil = require("oil") local oil = require("oil")
local test_util = require("tests.test_util") local test_util = require("tests.test_util")
local view = require("oil.view") local view = require("oil.view")
@ -12,7 +13,6 @@ a.describe("regression tests", function()
a.after_each(function() a.after_each(function()
if tmpdir then if tmpdir then
tmpdir:dispose() tmpdir:dispose()
a.util.scheduler()
tmpdir = nil tmpdir = nil
end end
test_util.reset_editor() test_util.reset_editor()
@ -117,4 +117,32 @@ a.describe("regression tests", function()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.are.same({ bufnr }, require("oil.view").get_all_buffers()) assert.are.same({ bufnr }, require("oil.view").get_all_buffers())
end) end)
a.it("can copy a file multiple times", function()
test_util.actions.open({ tmpdir.path })
vim.api.nvim_feedkeys("ifoo.txt", "x", true)
test_util.actions.save()
vim.api.nvim_feedkeys("yyp$ciWbar.txt", "x", true)
vim.api.nvim_feedkeys("yyp$ciWbaz.txt", "x", true)
test_util.actions.save()
assert.are.same({ "bar.txt", "baz.txt", "foo.txt" }, test_util.parse_entries(0))
tmpdir:assert_fs({
["foo.txt"] = "",
["bar.txt"] = "",
["baz.txt"] = "",
})
end)
-- https://github.com/stevearc/oil.nvim/issues/355
a.it("can open files from floating window", function()
tmpdir:create({ "a.txt" })
a.util.scheduler()
oil.open_float(tmpdir.path)
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
actions.select.callback()
vim.wait(1000, function()
return vim.fn.expand("%:t") == "a.txt"
end, 10)
assert.equals("a.txt", vim.fn.expand("%:t"))
end)
end) end)

View file

@ -8,8 +8,7 @@ a.describe("oil select", function()
end) end)
a.it("opens file under cursor", function() a.it("opens file under cursor", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
-- Go to the bottom, so the cursor is not on a directory -- Go to the bottom, so the cursor is not on a directory
vim.cmd.normal({ args = { "G" } }) vim.cmd.normal({ args = { "G" } })
a.wrap(oil.select, 2)() a.wrap(oil.select, 2)()
@ -18,8 +17,7 @@ a.describe("oil select", function()
end) end)
a.it("opens file in new tab", function() a.it("opens file in new tab", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
local tabpage = vim.api.nvim_get_current_tabpage() local tabpage = vim.api.nvim_get_current_tabpage()
a.wrap(oil.select, 2)({ tab = true }) a.wrap(oil.select, 2)({ tab = true })
assert.equals(2, #vim.api.nvim_list_tabpages()) assert.equals(2, #vim.api.nvim_list_tabpages())
@ -28,8 +26,7 @@ a.describe("oil select", function()
end) end)
a.it("opens file in new split", function() a.it("opens file in new split", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
a.wrap(oil.select, 2)({ vertical = true }) a.wrap(oil.select, 2)({ vertical = true })
assert.equals(1, #vim.api.nvim_list_tabpages()) assert.equals(1, #vim.api.nvim_list_tabpages())
@ -38,8 +35,7 @@ a.describe("oil select", function()
end) end)
a.it("opens multiple files in new tabs", function() a.it("opens multiple files in new tabs", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
vim.api.nvim_feedkeys("Vj", "x", true) vim.api.nvim_feedkeys("Vj", "x", true)
local tabpage = vim.api.nvim_get_current_tabpage() local tabpage = vim.api.nvim_get_current_tabpage()
a.wrap(oil.select, 2)({ tab = true }) a.wrap(oil.select, 2)({ tab = true })
@ -49,8 +45,7 @@ a.describe("oil select", function()
end) end)
a.it("opens multiple files in new splits", function() a.it("opens multiple files in new splits", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
vim.api.nvim_feedkeys("Vj", "x", true) vim.api.nvim_feedkeys("Vj", "x", true)
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
a.wrap(oil.select, 2)({ vertical = true }) a.wrap(oil.select, 2)({ vertical = true })
@ -63,8 +58,7 @@ a.describe("oil select", function()
a.it("same window", function() a.it("same window", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
-- Go to the bottom, so the cursor is not on a directory -- Go to the bottom, so the cursor is not on a directory
vim.cmd.normal({ args = { "G" } }) vim.cmd.normal({ args = { "G" } })
a.wrap(oil.select, 2)({ close = true }) a.wrap(oil.select, 2)({ close = true })
@ -79,8 +73,7 @@ a.describe("oil select", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
a.wrap(oil.select, 2)({ vertical = true, close = true }) a.wrap(oil.select, 2)({ vertical = true, close = true })
assert.equals(2, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(2, #vim.api.nvim_tabpage_list_wins(0))
assert.equals(bufnr, vim.api.nvim_win_get_buf(winid)) assert.equals(bufnr, vim.api.nvim_win_get_buf(winid))
@ -90,8 +83,7 @@ a.describe("oil select", function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "foo" } })
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local tabpage = vim.api.nvim_get_current_tabpage() local tabpage = vim.api.nvim_get_current_tabpage()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
a.wrap(oil.select, 2)({ tab = true, close = true }) a.wrap(oil.select, 2)({ tab = true, close = true })
assert.equals(1, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(1, #vim.api.nvim_tabpage_list_wins(0))
assert.equals(2, #vim.api.nvim_list_tabpages()) assert.equals(2, #vim.api.nvim_list_tabpages())

View file

@ -1,6 +1,7 @@
require("plenary.async").tests.add_to_env() require("plenary.async").tests.add_to_env()
local cache = require("oil.cache") local cache = require("oil.cache")
local test_adapter = require("oil.adapters.test") local test_adapter = require("oil.adapters.test")
local util = require("oil.util")
local M = {} local M = {}
M.reset_editor = function() M.reset_editor = function()
@ -25,6 +26,22 @@ M.reset_editor = function()
test_adapter.test_clear() test_adapter.test_clear()
end end
local function throwiferr(err, ...)
if err then
error(err)
else
return ...
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
M.wait_for_autocmd = a.wrap(function(autocmd, cb) M.wait_for_autocmd = a.wrap(function(autocmd, cb)
local opts = { local opts = {
pattern = "*", pattern = "*",
@ -41,6 +58,10 @@ M.wait_for_autocmd = a.wrap(function(autocmd, cb)
vim.api.nvim_create_autocmd(autocmd, opts) vim.api.nvim_create_autocmd(autocmd, opts)
end, 2) 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 actions string[]
---@param timestep integer ---@param timestep integer
M.feedkeys = function(actions, timestep) M.feedkeys = function(actions, timestep)
@ -58,4 +79,62 @@ M.feedkeys = function(actions, timestep)
a.util.sleep(timestep) a.util.sleep(timestep)
end end
M.actions = {
---Open oil and wait for it to finish rendering
---@param args string[]
open = function(args)
vim.schedule(function()
vim.cmd.Oil({ args = args })
-- If this buffer was already open, manually dispatch the autocmd to finish the wait
if vim.b.oil_ready then
vim.api.nvim_exec_autocmds("User", {
pattern = "OilEnter",
modeline = false,
data = { buf = vim.api.nvim_get_current_buf() },
})
end
end)
M.wait_for_autocmd({ "User", pattern = "OilEnter" })
end,
---Save all changes and wait for operation to complete
save = function()
vim.schedule_wrap(require("oil").save)({ confirm = false })
M.wait_for_autocmd({ "User", pattern = "OilMutationComplete" })
end,
---@param bufnr? integer
reload = function(bufnr)
M.await(require("oil.view").render_buffer_async, 3, bufnr or 0)
end,
---Move cursor to a file or directory in an oil buffer
---@param filename string
focus = function(filename)
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true)
local search = " " .. filename .. "$"
for i, line in ipairs(lines) do
if line:match(search) then
vim.api.nvim_win_set_cursor(0, { i, 0 })
return
end
end
error("Could not find file " .. filename)
end,
}
---Get the raw list of filenames from an unmodified oil buffer
---@param bufnr? integer
---@return string[]
M.parse_entries = function(bufnr)
bufnr = bufnr or 0
if vim.bo[bufnr].modified then
error("parse_entries doesn't work on a modified oil buffer")
end
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
return vim.tbl_map(function(line)
return line:match("^/%d+ +(.+)$")
end, lines)
end
return M return M

View file

@ -1,16 +1,7 @@
local fs = require("oil.fs") local fs = require("oil.fs")
local test_util = require("tests.test_util")
local function throwiferr(err, ...) local await = test_util.await
if err then
error(err)
else
return ...
end
end
local function await(fn, nargs, ...)
return throwiferr(a.wrap(fn, nargs)(...))
end
---@param path string ---@param path string
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
@ -41,6 +32,7 @@ local TmpDir = {}
TmpDir.new = function() TmpDir.new = function()
local path = await(vim.loop.fs_mkdtemp, 2, "oil_test_XXXXXXXXX") local path = await(vim.loop.fs_mkdtemp, 2, "oil_test_XXXXXXXXX")
a.util.scheduler()
return setmetatable({ path = path }, { return setmetatable({ path = path }, {
__index = TmpDir, __index = TmpDir,
}) })
@ -60,6 +52,7 @@ function TmpDir:create(paths)
end end
end end
end end
a.util.scheduler()
end end
---@param filepath string ---@param filepath string
@ -72,6 +65,7 @@ local read_file = function(filepath)
local stat = vim.loop.fs_fstat(fd) local stat = vim.loop.fs_fstat(fd)
local content = vim.loop.fs_read(fd, stat.size) local content = vim.loop.fs_read(fd, stat.size)
vim.loop.fs_close(fd) vim.loop.fs_close(fd)
a.util.scheduler()
return content return content
end end
@ -99,9 +93,9 @@ local assert_fs = function(root, paths)
local pieces = vim.split(k, "/") local pieces = vim.split(k, "/")
local partial_path = "" local partial_path = ""
for i, piece in ipairs(pieces) do for i, piece in ipairs(pieces) do
partial_path = fs.join(partial_path, piece) .. "/" partial_path = partial_path .. piece .. "/"
if i ~= #pieces then if i ~= #pieces then
unlisted_dirs[partial_path:sub(2)] = true unlisted_dirs[partial_path] = true
end end
end end
end end
@ -152,8 +146,23 @@ function TmpDir:assert_fs(paths)
assert_fs(self.path, paths) assert_fs(self.path, paths)
end end
function TmpDir:assert_exists(path)
a.util.scheduler()
path = fs.join(self.path, path)
local stat = vim.loop.fs_stat(path)
assert.truthy(stat, string.format("Expected path '%s' to exist", path))
end
function TmpDir:assert_not_exists(path)
a.util.scheduler()
path = fs.join(self.path, path)
local stat = vim.loop.fs_stat(path)
assert.falsy(stat, string.format("Expected path '%s' to not exist", path))
end
function TmpDir:dispose() function TmpDir:dispose()
await(fs.recursive_delete, 3, "directory", self.path) await(fs.recursive_delete, 3, "directory", self.path)
a.util.scheduler()
end end
return TmpDir return TmpDir

150
tests/trash_spec.lua Normal file
View file

@ -0,0 +1,150 @@
require("plenary.async").tests.add_to_env()
local TmpDir = require("tests.tmpdir")
local test_util = require("tests.test_util")
a.describe("freedesktop", function()
local tmpdir
local tmphome
local home = vim.env.XDG_DATA_HOME
a.before_each(function()
require("oil.config").delete_to_trash = true
tmpdir = TmpDir.new()
tmphome = TmpDir.new()
package.loaded["oil.adapters.trash"] = require("oil.adapters.trash.freedesktop")
vim.env.XDG_DATA_HOME = tmphome.path
end)
a.after_each(function()
vim.env.XDG_DATA_HOME = home
if tmpdir then
tmpdir:dispose()
end
if tmphome then
tmphome:dispose()
end
test_util.reset_editor()
package.loaded["oil.adapters.trash"] = nil
end)
a.it("files can be moved to the trash", function()
tmpdir:create({ "a.txt", "foo/b.txt" })
test_util.actions.open({ tmpdir.path })
test_util.actions.focus("a.txt")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.open({ "--trash", tmpdir.path })
vim.api.nvim_feedkeys("p", "x", true)
test_util.actions.save()
tmpdir:assert_not_exists("a.txt")
tmpdir:assert_exists("foo/b.txt")
test_util.actions.reload()
assert.are.same({ "a.txt" }, test_util.parse_entries(0))
end)
a.it("deleting a file moves it to trash", function()
tmpdir:create({ "a.txt", "foo/b.txt" })
test_util.actions.open({ tmpdir.path })
test_util.actions.focus("a.txt")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
tmpdir:assert_not_exists("a.txt")
tmpdir:assert_exists("foo/b.txt")
test_util.actions.open({ "--trash", tmpdir.path })
assert.are.same({ "a.txt" }, test_util.parse_entries(0))
end)
a.it("deleting a directory moves it to trash", function()
tmpdir:create({ "a.txt", "foo/b.txt" })
test_util.actions.open({ tmpdir.path })
test_util.actions.focus("foo/")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
tmpdir:assert_not_exists("foo")
tmpdir:assert_exists("a.txt")
test_util.actions.open({ "--trash", tmpdir.path })
assert.are.same({ "foo/" }, test_util.parse_entries(0))
end)
a.it("deleting a file from trash deletes it permanently", function()
tmpdir:create({ "a.txt" })
test_util.actions.open({ tmpdir.path })
test_util.actions.focus("a.txt")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
test_util.actions.open({ "--trash", tmpdir.path })
test_util.actions.focus("a.txt")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
test_util.actions.reload()
tmpdir:assert_not_exists("a.txt")
assert.are.same({}, test_util.parse_entries(0))
end)
a.it("cannot create files in the trash", function()
tmpdir:create({ "a.txt" })
test_util.actions.open({ tmpdir.path })
test_util.actions.focus("a.txt")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
test_util.actions.open({ "--trash", tmpdir.path })
vim.api.nvim_feedkeys("onew_file.txt", "x", true)
test_util.actions.save()
test_util.actions.reload()
assert.are.same({ "a.txt" }, test_util.parse_entries(0))
end)
a.it("cannot rename files in the trash", function()
tmpdir:create({ "a.txt" })
test_util.actions.open({ tmpdir.path })
test_util.actions.focus("a.txt")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
test_util.actions.open({ "--trash", tmpdir.path })
vim.api.nvim_feedkeys("0facwnew_name", "x", true)
test_util.actions.save()
test_util.actions.reload()
assert.are.same({ "a.txt" }, test_util.parse_entries(0))
end)
a.it("cannot copy files in the trash", function()
tmpdir:create({ "a.txt" })
test_util.actions.open({ tmpdir.path })
test_util.actions.focus("a.txt")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
test_util.actions.open({ "--trash", tmpdir.path })
vim.api.nvim_feedkeys("yypp", "x", true)
test_util.actions.save()
test_util.actions.reload()
assert.are.same({ "a.txt" }, test_util.parse_entries(0))
end)
a.it("can restore files from trash", function()
tmpdir:create({ "a.txt" })
test_util.actions.open({ tmpdir.path })
test_util.actions.focus("a.txt")
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
test_util.actions.open({ "--trash", tmpdir.path })
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.open({ tmpdir.path })
vim.api.nvim_feedkeys("p", "x", true)
test_util.actions.save()
test_util.actions.reload()
assert.are.same({ "a.txt" }, test_util.parse_entries(0))
tmpdir:assert_fs({
["a.txt"] = "a.txt",
})
end)
a.it("can have multiple files with the same name in trash", function()
tmpdir:create({ "a.txt" })
test_util.actions.open({ tmpdir.path })
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
tmpdir:create({ "a.txt" })
test_util.actions.reload()
vim.api.nvim_feedkeys("dd", "x", true)
test_util.actions.save()
test_util.actions.open({ "--trash", tmpdir.path })
assert.are.same({ "a.txt", "a.txt" }, test_util.parse_entries(0))
end)
end)

View file

@ -13,7 +13,7 @@ describe("url", function()
} }
for _, case in ipairs(cases) do for _, case in ipairs(cases) do
local input, expected, expected_basename = unpack(case) local input, expected, expected_basename = unpack(case)
local output, basename = oil.get_buffer_parent_url(input) local output, basename = oil.get_buffer_parent_url(input, true)
assert.equals(expected, output, string.format('Parent url for path "%s" failed', input)) assert.equals(expected, output, string.format('Parent url for path "%s" failed', input))
assert.equals( assert.equals(
expected_basename, expected_basename,

29
tests/util_spec.lua Normal file
View file

@ -0,0 +1,29 @@
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)

View file

@ -9,32 +9,28 @@ a.describe("window options", function()
a.it("Restores window options on close", function() a.it("Restores window options on close", function()
vim.cmd.edit({ args = { "README.md" } }) vim.cmd.edit({ args = { "README.md" } })
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
oil.close() oil.close()
assert.equals("auto", vim.o.signcolumn) assert.equals("auto", vim.o.signcolumn)
end) end)
a.it("Restores window options on edit", function() a.it("Restores window options on edit", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
vim.cmd.edit({ args = { "README.md" } }) vim.cmd.edit({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn) assert.equals("auto", vim.o.signcolumn)
end) end)
a.it("Restores window options on split <filename>", function() a.it("Restores window options on split <filename>", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
vim.cmd.split({ args = { "README.md" } }) vim.cmd.split({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn) assert.equals("auto", vim.o.signcolumn)
end) end)
a.it("Restores window options on split", function() a.it("Restores window options on split", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
vim.cmd.split() vim.cmd.split()
vim.cmd.edit({ args = { "README.md" } }) vim.cmd.edit({ args = { "README.md" } })
@ -42,16 +38,14 @@ a.describe("window options", function()
end) end)
a.it("Restores window options on tabnew <filename>", function() a.it("Restores window options on tabnew <filename>", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
vim.cmd.tabnew({ args = { "README.md" } }) vim.cmd.tabnew({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn) assert.equals("auto", vim.o.signcolumn)
end) end)
a.it("Restores window options on tabnew", function() a.it("Restores window options on tabnew", function()
oil.open() test_util.oil_open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("no", vim.o.signcolumn) assert.equals("no", vim.o.signcolumn)
vim.cmd.tabnew() vim.cmd.tabnew()
vim.cmd.edit({ args = { "README.md" } }) vim.cmd.edit({ args = { "README.md" } })